Merge branch 'dev' into teldufalsari/dev
This commit is contained in:
commit
c0cf852c62
@ -7,7 +7,7 @@
|
|||||||
- Custom client-side events and thier processing in VisionServer
|
- Custom client-side events and thier processing in VisionServer
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Color accessor property is now `colorProperty`. Color uses `invoke` instead of `set`
|
- Color accessor property is now `colorProperty`. Color uses non-nullable `invoke` instead of `set`.
|
||||||
- API update for server and pages
|
- API update for server and pages
|
||||||
- Edges moved to solids module for easier construction
|
- Edges moved to solids module for easier construction
|
||||||
- Visions **must** be rooted in order to subscribe to updates.
|
- Visions **must** be rooted in order to subscribe to updates.
|
||||||
|
@ -7,12 +7,12 @@ plugins {
|
|||||||
// id("org.jetbrains.kotlinx.kover") version "0.5.0"
|
// id("org.jetbrains.kotlinx.kover") version "0.5.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataforgeVersion by extra("0.6.2")
|
val dataforgeVersion by extra("0.7.1")
|
||||||
val fxVersion by extra("11")
|
val fxVersion by extra("11")
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.3.0-dev-14"
|
version = "0.3.0-dev-17"
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
@ -34,6 +34,6 @@ class GDMLVisionTest {
|
|||||||
val child = cubes[Name.of("composite-000","segment-0")]
|
val child = cubes[Name.of("composite-000","segment-0")]
|
||||||
assertNotNull(child)
|
assertNotNull(child)
|
||||||
child.properties.setValue(SolidMaterial.MATERIAL_COLOR_KEY, "red".asValue())
|
child.properties.setValue(SolidMaterial.MATERIAL_COLOR_KEY, "red".asValue())
|
||||||
assertEquals("red", child.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).string)
|
assertEquals("red", child.properties[SolidMaterial.MATERIAL_COLOR_KEY].string)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,7 +13,6 @@ kscience {
|
|||||||
useKtor()
|
useKtor()
|
||||||
fullStack(
|
fullStack(
|
||||||
"muon-monitor.js",
|
"muon-monitor.js",
|
||||||
development = true,
|
|
||||||
jvmConfig = { withJava() },
|
jvmConfig = { withJava() },
|
||||||
jsConfig = { useCommonJs() }
|
jsConfig = { useCommonJs() }
|
||||||
) {
|
) {
|
||||||
@ -47,9 +46,6 @@ application {
|
|||||||
mainClass.set("ru.mipt.npm.muon.monitor.server.MMServerKt")
|
mainClass.set("ru.mipt.npm.muon.monitor.server.MMServerKt")
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO ???
|
|
||||||
tasks.getByName("jsBrowserProductionWebpack").dependsOn("jsDevelopmentExecutableCompileSync")
|
|
||||||
|
|
||||||
//distributions {
|
//distributions {
|
||||||
// main {
|
// main {
|
||||||
// contents {
|
// contents {
|
||||||
|
@ -71,7 +71,7 @@ class Model(val manager: VisionManager) {
|
|||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
map.values.forEach {
|
map.values.forEach {
|
||||||
it.properties.setMeta(SolidMaterial.MATERIAL_COLOR_KEY, null)
|
it.properties[SolidMaterial.MATERIAL_COLOR_KEY] = null
|
||||||
}
|
}
|
||||||
tracks.children.clear()
|
tracks.children.clear()
|
||||||
}
|
}
|
||||||
|
@ -54,9 +54,6 @@
|
|||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"jupyter": {
|
|
||||||
"outputs_hidden": false
|
|
||||||
},
|
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@ -83,9 +80,6 @@
|
|||||||
"language": "kotlin",
|
"language": "kotlin",
|
||||||
"name": "kotlin"
|
"name": "kotlin"
|
||||||
},
|
},
|
||||||
"ktnbPluginMetadata": {
|
|
||||||
"isAddProjectLibrariesToClasspath": false
|
|
||||||
},
|
|
||||||
"language_info": {
|
"language_info": {
|
||||||
"codemirror_mode": "text/x-kotlin",
|
"codemirror_mode": "text/x-kotlin",
|
||||||
"file_extension": ".kt",
|
"file_extension": ".kt",
|
||||||
@ -94,6 +88,9 @@
|
|||||||
"nbconvert_exporter": "",
|
"nbconvert_exporter": "",
|
||||||
"pygments_lexer": "kotlin",
|
"pygments_lexer": "kotlin",
|
||||||
"version": "1.8.20"
|
"version": "1.8.20"
|
||||||
|
},
|
||||||
|
"ktnbPluginMetadata": {
|
||||||
|
"projectLibraries": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
|
45
demo/playground/notebooks/controls.ipynb
Normal file
45
demo/playground/notebooks/controls.ipynb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"USE(JupyterCommonIntegration())"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": [],
|
||||||
|
"source": [],
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Kotlin",
|
||||||
|
"language": "kotlin",
|
||||||
|
"name": "kotlin"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "kotlin",
|
||||||
|
"version": "1.9.0",
|
||||||
|
"mimetype": "text/x-kotlin",
|
||||||
|
"file_extension": ".kt",
|
||||||
|
"pygments_lexer": "kotlin",
|
||||||
|
"codemirror_mode": "text/x-kotlin",
|
||||||
|
"nbconvert_exporter": ""
|
||||||
|
},
|
||||||
|
"ktnbPluginMetadata": {
|
||||||
|
"projectDependencies": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 0
|
||||||
|
}
|
@ -25,10 +25,7 @@
|
|||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"collapsed": false,
|
"collapsed": false
|
||||||
"jupyter": {
|
|
||||||
"outputs_hidden": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
@ -84,7 +81,7 @@
|
|||||||
"version": "1.8.0-dev-3517"
|
"version": "1.8.0-dev-3517"
|
||||||
},
|
},
|
||||||
"ktnbPluginMetadata": {
|
"ktnbPluginMetadata": {
|
||||||
"isAddProjectLibrariesToClasspath": false
|
"projectLibraries": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
|
@ -75,7 +75,7 @@ fun main() {
|
|||||||
|
|
||||||
server.openInBrowser()
|
server.openInBrowser()
|
||||||
|
|
||||||
while (readln() != "exit") {
|
while (readlnOrNull() != "exit") {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,6 @@ org.gradle.jvmargs=-Xmx4G
|
|||||||
|
|
||||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||||
|
|
||||||
toolsVersion=0.15.0-kotlin-1.9.20-RC2
|
toolsVersion=0.15.2-kotlin-1.9.21
|
||||||
#kotlin.experimental.tryK2=true
|
#kotlin.experimental.tryK2=true
|
||||||
#kscience.wasm.disabled=true
|
#kscience.wasm.disabled=true
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -45,7 +45,7 @@ include(
|
|||||||
":ui:ring",
|
":ui:ring",
|
||||||
// ":ui:material",
|
// ":ui:material",
|
||||||
":ui:bootstrap",
|
":ui:bootstrap",
|
||||||
// ":ui:compose",
|
":ui:compose",
|
||||||
":visionforge-core",
|
":visionforge-core",
|
||||||
":visionforge-solid",
|
":visionforge-solid",
|
||||||
// ":visionforge-fx",
|
// ":visionforge-fx",
|
||||||
|
@ -29,7 +29,7 @@ public fun RBuilder.visionPropertyEditor(
|
|||||||
this.descriptor = descriptor
|
this.descriptor = descriptor
|
||||||
this.scope = vision.manager?.context ?: error("Orphan vision could not be observed")
|
this.scope = vision.manager?.context ?: error("Orphan vision could not be observed")
|
||||||
this.getPropertyState = { name ->
|
this.getPropertyState = { name ->
|
||||||
val ownMeta = vision.properties.own?.getMeta(name)
|
val ownMeta = vision.properties.own?.get(name)
|
||||||
if (ownMeta != null && !ownMeta.isEmpty()) {
|
if (ownMeta != null && !ownMeta.isEmpty()) {
|
||||||
EditorPropertyState.Defined
|
EditorPropertyState.Defined
|
||||||
} else if (vision.properties.root().getValue(name) != null) {
|
} else if (vision.properties.root().getValue(name) != null) {
|
||||||
|
42
ui/compose/build.gradle.kts
Normal file
42
ui/compose/build.gradle.kts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
plugins {
|
||||||
|
id("space.kscience.gradle.mpp")
|
||||||
|
alias(spclibs.plugins.compose)
|
||||||
|
// id("org.jetbrains.compose") version "1.5.11"
|
||||||
|
// id("com.android.library")
|
||||||
|
}
|
||||||
|
|
||||||
|
kscience{
|
||||||
|
jvm()
|
||||||
|
js()
|
||||||
|
// wasm()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
// android()
|
||||||
|
sourceSets {
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jvmMain by getting {
|
||||||
|
dependencies {
|
||||||
|
api(compose.runtime)
|
||||||
|
api(compose.foundation)
|
||||||
|
api(compose.material)
|
||||||
|
api(compose.preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsMain by getting{
|
||||||
|
dependencies {
|
||||||
|
api(compose.html.core)
|
||||||
|
api("app.softwork:bootstrap-compose:0.1.15")
|
||||||
|
api("app.softwork:bootstrap-compose-icons:0.1.15")
|
||||||
|
api(projects.visionforge.visionforgeThreejs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import org.jetbrains.compose.web.css.AlignItems
|
||||||
|
import org.jetbrains.compose.web.css.alignItems
|
||||||
|
import org.jetbrains.compose.web.dom.A
|
||||||
|
import org.jetbrains.compose.web.dom.Div
|
||||||
|
import org.jetbrains.compose.web.dom.Span
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
|
import space.kscience.dataforge.meta.descriptors.get
|
||||||
|
import space.kscience.dataforge.meta.get
|
||||||
|
import space.kscience.dataforge.meta.isLeaf
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.NameToken
|
||||||
|
import space.kscience.dataforge.names.lastOrNull
|
||||||
|
import space.kscience.dataforge.names.plus
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescriptor? = null) {
|
||||||
|
var expanded: Boolean by remember { mutableStateOf(true) }
|
||||||
|
val item: Meta? = root[name]
|
||||||
|
val descriptorItem: MetaDescriptor? = rootDescriptor?.get(name)
|
||||||
|
val actualValue = item?.value ?: descriptorItem?.defaultValue
|
||||||
|
val actualMeta = item ?: descriptorItem?.defaultNode
|
||||||
|
|
||||||
|
val token = name.lastOrNull()?.toString() ?: ""
|
||||||
|
|
||||||
|
FlexRow(attrs = {
|
||||||
|
classes("metaItem")
|
||||||
|
style {
|
||||||
|
alignItems(AlignItems.Center)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if (actualMeta?.isLeaf == false) {
|
||||||
|
Span({
|
||||||
|
classes(TreeStyles.treeCaret)
|
||||||
|
if (expanded) {
|
||||||
|
classes(TreeStyles.treeCaretDown)
|
||||||
|
}
|
||||||
|
onClick { expanded = !expanded }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Span({
|
||||||
|
classes(TreeStyles.treeLabel)
|
||||||
|
if (item == null) {
|
||||||
|
classes(TreeStyles.treeLabelInactive)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
Div {
|
||||||
|
A {
|
||||||
|
Text(actualValue.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expanded) {
|
||||||
|
FlexColumn({
|
||||||
|
classes(TreeStyles.tree)
|
||||||
|
}) {
|
||||||
|
val keys = buildSet {
|
||||||
|
descriptorItem?.children?.keys?.forEach {
|
||||||
|
add(NameToken(it))
|
||||||
|
}
|
||||||
|
actualMeta!!.items.keys.let { addAll(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.filter { !it.body.startsWith("@") }.forEach { token ->
|
||||||
|
Div({
|
||||||
|
classes(TreeStyles.treeItem)
|
||||||
|
}) {
|
||||||
|
MetaViewerItem(root, name + token, rootDescriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun MetaViewer(meta: Meta, descriptor: MetaDescriptor? = null) {
|
||||||
|
MetaViewerItem(meta, Name.EMPTY, descriptor)
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import org.jetbrains.compose.web.dom.Li
|
||||||
|
import org.jetbrains.compose.web.dom.Nav
|
||||||
|
import org.jetbrains.compose.web.dom.Ol
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.NameToken
|
||||||
|
import space.kscience.dataforge.names.length
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun NameCrumbs(name: Name?, link: (Name) -> Unit): Unit = Nav({
|
||||||
|
attr("aria-label","breadcrumb")
|
||||||
|
}) {
|
||||||
|
Ol({classes("breadcrumb")}) {
|
||||||
|
Li({
|
||||||
|
classes("breadcrumb-item")
|
||||||
|
onClick {
|
||||||
|
link(Name.EMPTY)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("\u2302")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name != null) {
|
||||||
|
val tokens = ArrayList<NameToken>(name.length)
|
||||||
|
name.tokens.forEach { token ->
|
||||||
|
tokens.add(token)
|
||||||
|
val fullName = Name(tokens.toList())
|
||||||
|
Text(".")
|
||||||
|
Li({
|
||||||
|
classes("breadcrumb-item")
|
||||||
|
if(tokens.size == name.length) classes("active")
|
||||||
|
onClick {
|
||||||
|
link(fullName)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(token.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jetbrains.compose.web.attributes.disabled
|
||||||
|
import org.jetbrains.compose.web.css.AlignItems
|
||||||
|
import org.jetbrains.compose.web.css.alignItems
|
||||||
|
import org.jetbrains.compose.web.css.px
|
||||||
|
import org.jetbrains.compose.web.css.width
|
||||||
|
import org.jetbrains.compose.web.dom.Button
|
||||||
|
import org.jetbrains.compose.web.dom.Div
|
||||||
|
import org.jetbrains.compose.web.dom.Span
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
|
import space.kscience.dataforge.meta.ObservableMutableMeta
|
||||||
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
|
import space.kscience.dataforge.meta.descriptors.ValueRestriction
|
||||||
|
import space.kscience.dataforge.meta.descriptors.get
|
||||||
|
import space.kscience.dataforge.meta.remove
|
||||||
|
import space.kscience.dataforge.names.*
|
||||||
|
import space.kscience.visionforge.hidden
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The display state of a property
|
||||||
|
*/
|
||||||
|
public sealed class EditorPropertyState {
|
||||||
|
public object Defined : EditorPropertyState()
|
||||||
|
public class Default(public val source: String = "unknown") : EditorPropertyState()
|
||||||
|
public object Undefined : EditorPropertyState()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param meta Root config object - always non-null
|
||||||
|
* @param rootDescriptor Full path to the displayed node in [meta]. Could be empty
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun PropertyEditor(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
meta: MutableMeta,
|
||||||
|
getPropertyState: (Name) -> EditorPropertyState,
|
||||||
|
updates: Flow<Name>,
|
||||||
|
name: Name = Name.EMPTY,
|
||||||
|
rootDescriptor: MetaDescriptor? = null,
|
||||||
|
initialExpanded: Boolean? = null,
|
||||||
|
) {
|
||||||
|
var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) }
|
||||||
|
val descriptor: MetaDescriptor? = remember(rootDescriptor, name) { rootDescriptor?.get(name) }
|
||||||
|
var property: MutableMeta by remember { mutableStateOf(meta.getOrCreate(name)) }
|
||||||
|
var editorPropertyState: EditorPropertyState by remember { mutableStateOf(getPropertyState(name)) }
|
||||||
|
|
||||||
|
|
||||||
|
val keys = remember(descriptor) {
|
||||||
|
buildSet {
|
||||||
|
descriptor?.children?.filterNot {
|
||||||
|
it.key.startsWith("@") || it.value.hidden
|
||||||
|
}?.forEach {
|
||||||
|
add(NameToken(it.key))
|
||||||
|
}
|
||||||
|
//ownProperty?.items?.keys?.filterNot { it.body.startsWith("@") }?.let { addAll(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = name.lastOrNull()?.toString() ?: "Properties"
|
||||||
|
|
||||||
|
fun update() {
|
||||||
|
property = meta.getOrCreate(name)
|
||||||
|
editorPropertyState = getPropertyState(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(meta) {
|
||||||
|
updates.collect { updatedName ->
|
||||||
|
if (updatedName == name) {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlexRow({
|
||||||
|
style {
|
||||||
|
alignItems(AlignItems.Center)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if (keys.isNotEmpty()) {
|
||||||
|
Span({
|
||||||
|
classes(TreeStyles.treeCaret)
|
||||||
|
if (expanded) {
|
||||||
|
classes(TreeStyles.treeCaretDown)
|
||||||
|
}
|
||||||
|
onClick { expanded = !expanded }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Span({
|
||||||
|
classes(TreeStyles.treeLabel)
|
||||||
|
if (editorPropertyState != EditorPropertyState.Defined) {
|
||||||
|
classes(TreeStyles.treeLabelInactive)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) {
|
||||||
|
Div({
|
||||||
|
style {
|
||||||
|
width(160.px)
|
||||||
|
marginAll(1.px, 5.px)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
ValueChooser(descriptor, editorPropertyState, property.value) {
|
||||||
|
property.value = it
|
||||||
|
editorPropertyState = getPropertyState(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button({
|
||||||
|
classes(TreeStyles.propertyEditorButton)
|
||||||
|
if (editorPropertyState != EditorPropertyState.Defined) {
|
||||||
|
disabled()
|
||||||
|
} else {
|
||||||
|
onClick {
|
||||||
|
meta.remove(name)
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("\u00D7")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expanded) {
|
||||||
|
FlexColumn({
|
||||||
|
classes(TreeStyles.tree)
|
||||||
|
}) {
|
||||||
|
keys.forEach { token ->
|
||||||
|
Div({
|
||||||
|
classes(TreeStyles.treeItem)
|
||||||
|
}) {
|
||||||
|
PropertyEditor(scope, meta, getPropertyState, updates, name + token, descriptor, expanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun PropertyEditor(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
properties: ObservableMutableMeta,
|
||||||
|
descriptor: MetaDescriptor? = null,
|
||||||
|
expanded: Boolean? = null,
|
||||||
|
) {
|
||||||
|
PropertyEditor(
|
||||||
|
scope = scope,
|
||||||
|
meta = properties,
|
||||||
|
getPropertyState = { name ->
|
||||||
|
if (properties[name] != null) {
|
||||||
|
EditorPropertyState.Defined
|
||||||
|
} else if (descriptor?.get(name)?.defaultValue != null) {
|
||||||
|
EditorPropertyState.Default("descriptor")
|
||||||
|
} else {
|
||||||
|
EditorPropertyState.Undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updates = callbackFlow {
|
||||||
|
properties.onChange(scope) { name ->
|
||||||
|
scope.launch {
|
||||||
|
send(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeOnClose {
|
||||||
|
properties.removeListener(scope)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name = Name.EMPTY,
|
||||||
|
rootDescriptor = descriptor,
|
||||||
|
initialExpanded = expanded,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import org.jetbrains.compose.web.dom.*
|
||||||
|
import org.w3c.dom.HTMLDivElement
|
||||||
|
import org.w3c.dom.HTMLLIElement
|
||||||
|
|
||||||
|
|
||||||
|
public class ComposeTab(
|
||||||
|
public val key: String,
|
||||||
|
public val title: String,
|
||||||
|
public val content: ContentBuilder<HTMLDivElement>,
|
||||||
|
public val disabled: Boolean,
|
||||||
|
public val titleExt: ContentBuilder<HTMLLIElement>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Tabs(tabs: List<ComposeTab>, activeKey: String) {
|
||||||
|
var active by remember(activeKey) { mutableStateOf(activeKey) }
|
||||||
|
|
||||||
|
Div({ classes("card", "text-center") }) {
|
||||||
|
Div({ classes("card-header") }) {
|
||||||
|
|
||||||
|
Ul({ classes("nav", "nav-tabs", "card-header-tabs") }) {
|
||||||
|
tabs.forEach { tab ->
|
||||||
|
Li({
|
||||||
|
classes("nav-item")
|
||||||
|
}) {
|
||||||
|
A(attrs = {
|
||||||
|
classes("nav-link")
|
||||||
|
if (active == tab.key) {
|
||||||
|
classes("active")
|
||||||
|
}
|
||||||
|
if (tab.disabled) {
|
||||||
|
classes("disabled")
|
||||||
|
}
|
||||||
|
onClick {
|
||||||
|
active = tab.key
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(tab.title)
|
||||||
|
}
|
||||||
|
tab.titleExt.invoke(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabs.find { it.key == active }?.let { tab ->
|
||||||
|
Div({ classes("card-body") }) {
|
||||||
|
tab.content.invoke(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TabBuilder internal constructor(public val key: String) {
|
||||||
|
private var title: String = key
|
||||||
|
public var disabled: Boolean = false
|
||||||
|
private var content: ContentBuilder<HTMLDivElement> = {}
|
||||||
|
private var titleExt: ContentBuilder<HTMLLIElement> = {}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Content(content: ContentBuilder<HTMLDivElement>) {
|
||||||
|
this.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Title(title: String, titleExt: ContentBuilder<HTMLLIElement> = {}) {
|
||||||
|
this.title = title
|
||||||
|
this.titleExt = titleExt
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun build(): ComposeTab = ComposeTab(
|
||||||
|
key,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
disabled,
|
||||||
|
titleExt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TabsBuilder {
|
||||||
|
public var active: String = ""
|
||||||
|
internal val tabs: MutableList<ComposeTab> = mutableListOf()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Tab(key: String, builder: @Composable TabBuilder.() -> Unit) {
|
||||||
|
tabs.add(TabBuilder(key).apply { builder() }.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun addTab(tab: ComposeTab) {
|
||||||
|
tabs.add(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Tabs(builder: @Composable TabsBuilder.() -> Unit) {
|
||||||
|
val result = TabsBuilder().apply { builder() }
|
||||||
|
Tabs(result.tabs, result.active)
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import kotlinx.dom.clear
|
||||||
|
import org.jetbrains.compose.web.css.*
|
||||||
|
import org.jetbrains.compose.web.dom.Div
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.visionforge.solid.Solid
|
||||||
|
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
|
||||||
|
import space.kscience.visionforge.solid.three.ThreeCanvas
|
||||||
|
import space.kscience.visionforge.solid.three.ThreePlugin
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun ThreeCanvas(
|
||||||
|
context: Context,
|
||||||
|
options: Canvas3DOptions?,
|
||||||
|
solid: Solid?,
|
||||||
|
selected: Name?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val three: ThreePlugin by derivedStateOf { context.request(ThreePlugin) }
|
||||||
|
|
||||||
|
Div({
|
||||||
|
style {
|
||||||
|
maxWidth(100.vw)
|
||||||
|
maxHeight(100.vh)
|
||||||
|
width(100.percent)
|
||||||
|
height(100.percent)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
var canvas: ThreeCanvas? = null
|
||||||
|
DisposableEffect(options) {
|
||||||
|
canvas = ThreeCanvas(three, scopeElement, options ?: Canvas3DOptions())
|
||||||
|
onDispose {
|
||||||
|
scopeElement.clear()
|
||||||
|
canvas = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(solid) {
|
||||||
|
if (solid != null) {
|
||||||
|
canvas?.render(solid)
|
||||||
|
} else {
|
||||||
|
canvas?.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(selected) {
|
||||||
|
canvas?.select(selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import org.jetbrains.compose.web.css.*
|
||||||
|
import org.jetbrains.compose.web.dom.Button
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
import org.w3c.files.Blob
|
||||||
|
import org.w3c.files.BlobPropertyBag
|
||||||
|
import space.kscience.dataforge.context.Global
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.visionforge.Vision
|
||||||
|
import space.kscience.visionforge.encodeToString
|
||||||
|
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun CanvasControls(
|
||||||
|
vision: Vision?,
|
||||||
|
options: Canvas3DOptions,
|
||||||
|
) {
|
||||||
|
FlexColumn {
|
||||||
|
FlexRow({
|
||||||
|
style {
|
||||||
|
border {
|
||||||
|
width(1.px)
|
||||||
|
style(LineStyle.Solid)
|
||||||
|
color(Color("blue"))
|
||||||
|
}
|
||||||
|
padding(4.px)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
vision?.let { vision ->
|
||||||
|
Button({
|
||||||
|
onClick { event ->
|
||||||
|
val json = vision.encodeToString()
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
val fileSaver = kotlinext.js.require<dynamic>("file-saver")
|
||||||
|
val blob = Blob(arrayOf(json), BlobPropertyBag("text/json;charset=utf-8"))
|
||||||
|
fileSaver.saveAs(blob, "object.json") as Unit
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Export")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PropertyEditor(
|
||||||
|
scope = vision?.manager?.context ?: Global,
|
||||||
|
properties = options.meta,
|
||||||
|
descriptor = Canvas3DOptions.descriptor,
|
||||||
|
expanded = false
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun ThreeControls(
|
||||||
|
vision: Vision?,
|
||||||
|
canvasOptions: Canvas3DOptions,
|
||||||
|
selected: Name?,
|
||||||
|
onSelect: (Name?) -> Unit,
|
||||||
|
tabBuilder: @Composable TabsBuilder.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
Tabs {
|
||||||
|
active = "Tree"
|
||||||
|
vision?.let { vision ->
|
||||||
|
Tab("Tree") {
|
||||||
|
CardTitle("Vision tree")
|
||||||
|
VisionTree(vision, Name.EMPTY, selected, onSelect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tab("Settings") {
|
||||||
|
CardTitle("Canvas configuration")
|
||||||
|
CanvasControls(vision, canvasOptions)
|
||||||
|
}
|
||||||
|
tabBuilder()
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
package space.kscience.visionforge.compose
|
|
||||||
|
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
public fun ThreeJs(){
|
|
||||||
Surface {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,169 @@
|
|||||||
|
@file:OptIn(ExperimentalComposeWebApi::class)
|
||||||
|
|
||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import app.softwork.bootstrapcompose.Card
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jetbrains.compose.web.ExperimentalComposeWebApi
|
||||||
|
import org.jetbrains.compose.web.css.*
|
||||||
|
import org.jetbrains.compose.web.dom.*
|
||||||
|
import space.kscience.dataforge.meta.get
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.isEmpty
|
||||||
|
import space.kscience.visionforge.*
|
||||||
|
import space.kscience.visionforge.solid.Solid
|
||||||
|
import space.kscience.visionforge.solid.SolidGroup
|
||||||
|
import space.kscience.visionforge.solid.Solids
|
||||||
|
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun ThreeCanvasWithControls(
|
||||||
|
solids: Solids,
|
||||||
|
builderOfSolid: Deferred<Solid?>,
|
||||||
|
initialSelected: Name?,
|
||||||
|
options: Canvas3DOptions?,
|
||||||
|
tabBuilder: @Composable TabsBuilder.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
var selected: Name? by remember { mutableStateOf(initialSelected) }
|
||||||
|
var solid: Solid? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(builderOfSolid) {
|
||||||
|
solids.context.launch {
|
||||||
|
solid = builderOfSolid.await()
|
||||||
|
//ensure that the solid is properly rooted
|
||||||
|
if (solid?.parent == null) {
|
||||||
|
solid?.setAsRoot(solids.context.visionManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val optionsWithSelector = remember(options) {
|
||||||
|
(options ?: Canvas3DOptions()).apply {
|
||||||
|
this.onSelect = {
|
||||||
|
selected = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedVision: Vision? = remember(builderOfSolid, selected) {
|
||||||
|
selected?.let {
|
||||||
|
when {
|
||||||
|
it.isEmpty() -> solid
|
||||||
|
else -> (solid as? SolidGroup)?.get(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FlexRow({
|
||||||
|
style {
|
||||||
|
height(100.percent)
|
||||||
|
width(100.percent)
|
||||||
|
flexWrap(FlexWrap.Wrap)
|
||||||
|
alignItems(AlignItems.Stretch)
|
||||||
|
alignContent(AlignContent.Stretch)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
FlexColumn({
|
||||||
|
style {
|
||||||
|
height(100.percent)
|
||||||
|
minWidth(600.px)
|
||||||
|
flex(10, 1, 600.px)
|
||||||
|
position(Position.Relative)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if (solid == null) {
|
||||||
|
Div({
|
||||||
|
style {
|
||||||
|
position(Position.Fixed)
|
||||||
|
width(100.percent)
|
||||||
|
height(100.percent)
|
||||||
|
zIndex(1000)
|
||||||
|
top(40.percent)
|
||||||
|
left(0.px)
|
||||||
|
opacity(0.5)
|
||||||
|
filter {
|
||||||
|
opacity(50.percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Div({ classes("d-flex", " justify-content-center") }) {
|
||||||
|
Div({
|
||||||
|
classes("spinner-grow", "text-primary")
|
||||||
|
style {
|
||||||
|
width(3.cssRem)
|
||||||
|
height(3.cssRem)
|
||||||
|
zIndex(20)
|
||||||
|
}
|
||||||
|
attr("role", "status")
|
||||||
|
}) {
|
||||||
|
Span({ classes("sr-only") }) { Text("Loading 3D vision") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ThreeCanvas(solids.context, optionsWithSelector, solid, selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedVision?.let { vision ->
|
||||||
|
Div({
|
||||||
|
style {
|
||||||
|
position(Position.Absolute)
|
||||||
|
top(5.px)
|
||||||
|
right(5.px)
|
||||||
|
width(450.px)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Card(
|
||||||
|
headerAttrs = {
|
||||||
|
// border = true
|
||||||
|
},
|
||||||
|
header = {
|
||||||
|
NameCrumbs(selected) { selected = it }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
PropertyEditor(
|
||||||
|
scope = solids.context,
|
||||||
|
meta = vision.properties.root(),
|
||||||
|
getPropertyState = { name ->
|
||||||
|
if (vision.properties.own?.get(name) != null) {
|
||||||
|
EditorPropertyState.Defined
|
||||||
|
} else if (vision.properties.root()[name] != null) {
|
||||||
|
// TODO differentiate
|
||||||
|
EditorPropertyState.Default()
|
||||||
|
} else {
|
||||||
|
EditorPropertyState.Undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updates = vision.properties.changes,
|
||||||
|
rootDescriptor = vision.descriptor
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
vision.styles.takeIf { it.isNotEmpty() }?.let { styles ->
|
||||||
|
P {
|
||||||
|
B { Text("Styles: ") }
|
||||||
|
Text(styles.joinToString(separator = ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlexColumn({
|
||||||
|
style {
|
||||||
|
paddingAll(4.px)
|
||||||
|
minWidth(400.px)
|
||||||
|
height(100.percent)
|
||||||
|
overflowY("auto")
|
||||||
|
flex(1, 10, 300.px)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
ThreeControls(solid, optionsWithSelector, selected, onSelect = { selected = it }, tabBuilder = tabBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import org.jetbrains.compose.web.ExperimentalComposeWebApi
|
||||||
|
import org.jetbrains.compose.web.css.*
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeWebApi::class)
|
||||||
|
public object TreeStyles : StyleSheet() {
|
||||||
|
/**
|
||||||
|
* Remove default bullets
|
||||||
|
*/
|
||||||
|
public val tree: String by style {
|
||||||
|
paddingLeft(5.px)
|
||||||
|
marginLeft(0.px)
|
||||||
|
listStyleType("none")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style the caret/arrow
|
||||||
|
*/
|
||||||
|
public val treeCaret: String by style {
|
||||||
|
cursor("pointer")
|
||||||
|
userSelect(UserSelect.none)
|
||||||
|
/* Create the caret/arrow with a unicode, and style it */
|
||||||
|
before {
|
||||||
|
content("\u25B6")
|
||||||
|
color(Color.black)
|
||||||
|
display(DisplayStyle.InlineBlock)
|
||||||
|
marginRight(6.px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate the caret/arrow icon when clicked on (using JavaScript)
|
||||||
|
*/
|
||||||
|
public val treeCaretDown: String by style {
|
||||||
|
before {
|
||||||
|
content("\u25B6")
|
||||||
|
color(Color.black)
|
||||||
|
display(DisplayStyle.InlineBlock)
|
||||||
|
marginRight(6.px)
|
||||||
|
transform { rotate(90.deg) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public val treeItem: String by style {
|
||||||
|
alignItems(AlignItems.Center)
|
||||||
|
paddingLeft(10.px)
|
||||||
|
border {
|
||||||
|
left {
|
||||||
|
width(1.px)
|
||||||
|
color(Color.lightgray)
|
||||||
|
style = LineStyle.Dashed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public val treeLabel: String by style {
|
||||||
|
border(style = LineStyle.None)
|
||||||
|
paddingAll(left = 4.pt, right = 4.pt)
|
||||||
|
textAlign("left")
|
||||||
|
flex(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
public val treeLabelInactive: String by style {
|
||||||
|
color(Color.lightgray)
|
||||||
|
}
|
||||||
|
|
||||||
|
public val treeLabelSelected: String by style {
|
||||||
|
backgroundColor(Color.lightblue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public val propertyEditorButton: String by style {
|
||||||
|
width(24.px)
|
||||||
|
alignSelf(AlignSelf.Stretch)
|
||||||
|
marginAll(1.px, 5.px)
|
||||||
|
backgroundColor(Color.white)
|
||||||
|
border{
|
||||||
|
style(LineStyle.Solid)
|
||||||
|
}
|
||||||
|
borderRadius(2.px)
|
||||||
|
textAlign("center")
|
||||||
|
textDecoration("none")
|
||||||
|
cursor("pointer")
|
||||||
|
disabled {
|
||||||
|
cursor("auto")
|
||||||
|
border{
|
||||||
|
style(LineStyle.Dashed)
|
||||||
|
}
|
||||||
|
color(Color.lightgray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import org.jetbrains.compose.web.css.Color
|
||||||
|
import org.jetbrains.compose.web.css.color
|
||||||
|
import org.jetbrains.compose.web.css.cursor
|
||||||
|
import org.jetbrains.compose.web.css.textDecorationLine
|
||||||
|
import org.jetbrains.compose.web.dom.Div
|
||||||
|
import org.jetbrains.compose.web.dom.Span
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.lastOrNull
|
||||||
|
import space.kscience.dataforge.names.plus
|
||||||
|
import space.kscience.dataforge.names.startsWith
|
||||||
|
import space.kscience.visionforge.Vision
|
||||||
|
import space.kscience.visionforge.VisionGroup
|
||||||
|
import space.kscience.visionforge.asSequence
|
||||||
|
import space.kscience.visionforge.compose.TreeStyles.hover
|
||||||
|
import space.kscience.visionforge.compose.TreeStyles.invoke
|
||||||
|
import space.kscience.visionforge.isEmpty
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TreeLabel(
|
||||||
|
vision: Vision,
|
||||||
|
name: Name,
|
||||||
|
selected: Name?,
|
||||||
|
clickCallback: (Name) -> Unit,
|
||||||
|
) {
|
||||||
|
Span({
|
||||||
|
classes(TreeStyles.treeLabel)
|
||||||
|
if (name == selected) {
|
||||||
|
classes(TreeStyles.treeLabelSelected)
|
||||||
|
}
|
||||||
|
style {
|
||||||
|
color(Color("#069"))
|
||||||
|
cursor("pointer")
|
||||||
|
hover.invoke {
|
||||||
|
textDecorationLine("underline")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
onClick { clickCallback(name) }
|
||||||
|
}) {
|
||||||
|
Text(name.lastOrNull()?.toString() ?: "World")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun VisionTree(
|
||||||
|
vision: Vision,
|
||||||
|
name: Name = Name.EMPTY,
|
||||||
|
selected: Name? = null,
|
||||||
|
clickCallback: (Name) -> Unit,
|
||||||
|
): Unit {
|
||||||
|
var expanded: Boolean by remember { mutableStateOf(selected?.startsWith(name) ?: false) }
|
||||||
|
|
||||||
|
//display as node if any child is visible
|
||||||
|
if (vision is VisionGroup) {
|
||||||
|
FlexRow {
|
||||||
|
if (vision.children.keys.any { !it.body.startsWith("@") }) {
|
||||||
|
Span({
|
||||||
|
classes(TreeStyles.treeCaret)
|
||||||
|
if (expanded) {
|
||||||
|
classes(TreeStyles.treeCaretDown)
|
||||||
|
}
|
||||||
|
onClick {
|
||||||
|
expanded = !expanded
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
TreeLabel(vision, name, selected, clickCallback)
|
||||||
|
}
|
||||||
|
if (expanded) {
|
||||||
|
FlexColumn({
|
||||||
|
classes(TreeStyles.tree)
|
||||||
|
}) {
|
||||||
|
vision.children.asSequence()
|
||||||
|
.filter { !it.first.toString().startsWith("@") } // ignore statics and other hidden children
|
||||||
|
.sortedBy { (it.second as? VisionGroup)?.children?.isEmpty() ?: true } // ignore empty groups
|
||||||
|
.forEach { (childToken, child) ->
|
||||||
|
Div({ classes(TreeStyles.treeItem) }) {
|
||||||
|
VisionTree(
|
||||||
|
child,
|
||||||
|
name + childToken,
|
||||||
|
selected,
|
||||||
|
clickCallback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TreeLabel(vision, name, selected, clickCallback)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import org.jetbrains.compose.web.dom.H5
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun CardTitle(title: String): Unit = H5({ classes("card-title") }) { Text(title) }
|
@ -0,0 +1,44 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import org.jetbrains.compose.web.css.*
|
||||||
|
import org.jetbrains.compose.web.css.keywords.CSSAutoKeyword
|
||||||
|
|
||||||
|
public enum class UserSelect {
|
||||||
|
inherit, initial, revert, revertLayer, unset,
|
||||||
|
|
||||||
|
none, auto, text, contain, all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun StyleScope.userSelect(value: UserSelect) {
|
||||||
|
property("user-select", value.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun StyleScope.content(value: String) {
|
||||||
|
property("content", "'$value'")
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun StyleScope.paddingAll(
|
||||||
|
top: CSSNumeric = 0.pt,
|
||||||
|
right: CSSNumeric = top,
|
||||||
|
bottom: CSSNumeric = top,
|
||||||
|
left: CSSNumeric = right,
|
||||||
|
) {
|
||||||
|
padding(top, right, bottom, left)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun StyleScope.marginAll(
|
||||||
|
top: CSSNumeric = 0.pt,
|
||||||
|
right: CSSNumeric = top,
|
||||||
|
bottom: CSSNumeric = top,
|
||||||
|
left: CSSNumeric = right,
|
||||||
|
) {
|
||||||
|
margin(top, right, bottom, left)
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun StyleScope.zIndex(value: Int) {
|
||||||
|
property("z-index", "$value")
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun StyleScope.zIndex(value: CSSAutoKeyword) {
|
||||||
|
property("z-index", value)
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import org.jetbrains.compose.web.css.DisplayStyle
|
||||||
|
import org.jetbrains.compose.web.css.FlexDirection
|
||||||
|
import org.jetbrains.compose.web.css.display
|
||||||
|
import org.jetbrains.compose.web.css.flexDirection
|
||||||
|
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||||
|
import org.jetbrains.compose.web.dom.Div
|
||||||
|
import org.jetbrains.compose.web.dom.ElementScope
|
||||||
|
import org.w3c.dom.HTMLDivElement
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun FlexColumn(
|
||||||
|
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||||
|
content: @Composable ElementScope<HTMLDivElement>.() -> Unit,
|
||||||
|
): Unit = Div(
|
||||||
|
attrs = {
|
||||||
|
style {
|
||||||
|
display(DisplayStyle.Flex)
|
||||||
|
flexDirection(FlexDirection.Column)
|
||||||
|
}
|
||||||
|
attrs?.invoke(this)
|
||||||
|
},
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun FlexRow(
|
||||||
|
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||||
|
content: @Composable ElementScope<HTMLDivElement>.() -> Unit,
|
||||||
|
): Unit = Div(
|
||||||
|
attrs = {
|
||||||
|
style {
|
||||||
|
display(DisplayStyle.Flex)
|
||||||
|
flexDirection(FlexDirection.Row)
|
||||||
|
}
|
||||||
|
attrs?.invoke(this)
|
||||||
|
},
|
||||||
|
content
|
||||||
|
)
|
@ -0,0 +1,268 @@
|
|||||||
|
@file:Suppress("UNUSED_PARAMETER")
|
||||||
|
|
||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import org.jetbrains.compose.web.attributes.*
|
||||||
|
import org.jetbrains.compose.web.css.percent
|
||||||
|
import org.jetbrains.compose.web.css.px
|
||||||
|
import org.jetbrains.compose.web.css.width
|
||||||
|
import org.jetbrains.compose.web.dom.Input
|
||||||
|
import org.jetbrains.compose.web.dom.Option
|
||||||
|
import org.jetbrains.compose.web.dom.Select
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
import org.w3c.dom.HTMLOptionElement
|
||||||
|
import org.w3c.dom.asList
|
||||||
|
import space.kscience.dataforge.meta.*
|
||||||
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
|
import space.kscience.dataforge.meta.descriptors.ValueRestriction
|
||||||
|
import space.kscience.dataforge.meta.descriptors.allowedValues
|
||||||
|
import space.kscience.visionforge.Colors
|
||||||
|
import space.kscience.visionforge.widgetType
|
||||||
|
import three.math.Color
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun StringValueChooser(
|
||||||
|
descriptor: MetaDescriptor?,
|
||||||
|
state: EditorPropertyState,
|
||||||
|
value: Value?,
|
||||||
|
onValueChange: (Value?) -> Unit,
|
||||||
|
) {
|
||||||
|
var stringValue by remember { mutableStateOf(value?.string ?: "") }
|
||||||
|
Input(type = InputType.Text) {
|
||||||
|
style {
|
||||||
|
width(100.percent)
|
||||||
|
}
|
||||||
|
value(stringValue)
|
||||||
|
onKeyDown { event ->
|
||||||
|
if (event.type == "keydown" && event.asDynamic().key == "Enter") {
|
||||||
|
stringValue = (event.target as HTMLInputElement).value
|
||||||
|
onValueChange(stringValue.asValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChange {
|
||||||
|
stringValue = it.target.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun BooleanValueChooser(
|
||||||
|
descriptor: MetaDescriptor?,
|
||||||
|
state: EditorPropertyState,
|
||||||
|
value: Value?,
|
||||||
|
onValueChange: (Value?) -> Unit,
|
||||||
|
) {
|
||||||
|
Input(type = InputType.Checkbox) {
|
||||||
|
style {
|
||||||
|
width(100.percent)
|
||||||
|
}
|
||||||
|
//this.attributes["indeterminate"] = (props.item == null).toString()
|
||||||
|
checked(value?.boolean ?: false)
|
||||||
|
|
||||||
|
onChange {
|
||||||
|
val newValue = it.target.checked
|
||||||
|
onValueChange(newValue.asValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun NumberValueChooser(
|
||||||
|
descriptor: MetaDescriptor?,
|
||||||
|
state: EditorPropertyState,
|
||||||
|
value: Value?,
|
||||||
|
onValueChange: (Value?) -> Unit,
|
||||||
|
) {
|
||||||
|
var innerValue by remember { mutableStateOf(value?.string ?: "") }
|
||||||
|
Input(type = InputType.Number) {
|
||||||
|
style {
|
||||||
|
width(100.percent)
|
||||||
|
}
|
||||||
|
value(innerValue)
|
||||||
|
onKeyDown { event ->
|
||||||
|
if (event.type == "keydown" && event.asDynamic().key == "Enter") {
|
||||||
|
innerValue = (event.target as HTMLInputElement).value
|
||||||
|
val number = innerValue.toDoubleOrNull()
|
||||||
|
if (number == null) {
|
||||||
|
console.error("The input value $innerValue is not a number")
|
||||||
|
} else {
|
||||||
|
onValueChange(number.asValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChange {
|
||||||
|
innerValue = it.target.value
|
||||||
|
}
|
||||||
|
descriptor?.attributes?.get("step").number?.let {
|
||||||
|
step(it)
|
||||||
|
}
|
||||||
|
descriptor?.attributes?.get("min").string?.let {
|
||||||
|
min(it)
|
||||||
|
}
|
||||||
|
descriptor?.attributes?.get("max").string?.let {
|
||||||
|
max(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun ComboValueChooser(
|
||||||
|
descriptor: MetaDescriptor?,
|
||||||
|
state: EditorPropertyState,
|
||||||
|
value: Value?,
|
||||||
|
onValueChange: (Value?) -> Unit,
|
||||||
|
) {
|
||||||
|
var selected by remember { mutableStateOf(value?.string ?: "") }
|
||||||
|
Select({
|
||||||
|
style {
|
||||||
|
width(100.percent)
|
||||||
|
}
|
||||||
|
onChange {
|
||||||
|
selected = it.target.value
|
||||||
|
onValueChange(selected.asValue())
|
||||||
|
}
|
||||||
|
}, multiple = false) {
|
||||||
|
descriptor?.allowedValues?.forEach {
|
||||||
|
Option(it.string, { if (it == value) selected() }) {
|
||||||
|
Text(it.string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun ColorValueChooser(
|
||||||
|
descriptor: MetaDescriptor?,
|
||||||
|
state: EditorPropertyState,
|
||||||
|
value: Value?,
|
||||||
|
onValueChange: (Value?) -> Unit,
|
||||||
|
) {
|
||||||
|
Input(type = InputType.Color) {
|
||||||
|
style {
|
||||||
|
width(100.percent)
|
||||||
|
marginAll(0.px)
|
||||||
|
}
|
||||||
|
value(
|
||||||
|
value?.let { value ->
|
||||||
|
if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int)
|
||||||
|
else "#" + Color(value.string).getHexString()
|
||||||
|
} ?: "#000000"
|
||||||
|
)
|
||||||
|
onChange {
|
||||||
|
onValueChange(it.target.value.asValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun MultiSelectChooser(
|
||||||
|
descriptor: MetaDescriptor?,
|
||||||
|
state: EditorPropertyState,
|
||||||
|
value: Value?,
|
||||||
|
onValueChange: (Value?) -> Unit,
|
||||||
|
) {
|
||||||
|
Select({
|
||||||
|
onChange { event ->
|
||||||
|
val newSelected = event.target.selectedOptions.asList()
|
||||||
|
.map { (it as HTMLOptionElement).value.asValue() }
|
||||||
|
onValueChange(newSelected.asValue())
|
||||||
|
|
||||||
|
}
|
||||||
|
}, multiple = true) {
|
||||||
|
descriptor?.allowedValues?.forEach { optionValue ->
|
||||||
|
Option(optionValue.string, {
|
||||||
|
value?.list?.let { if (optionValue in it) selected() }
|
||||||
|
}) {
|
||||||
|
Text(optionValue.string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun RangeValueChooser(
|
||||||
|
descriptor: MetaDescriptor?,
|
||||||
|
state: EditorPropertyState,
|
||||||
|
value: Value?,
|
||||||
|
onValueChange: (Value?) -> Unit,
|
||||||
|
) {
|
||||||
|
var innerValue by remember { mutableStateOf(value?.double) }
|
||||||
|
var rangeDisabled: Boolean by remember { mutableStateOf(state != EditorPropertyState.Defined) }
|
||||||
|
|
||||||
|
|
||||||
|
FlexRow {
|
||||||
|
if (descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
|
||||||
|
Input(type = InputType.Checkbox) {
|
||||||
|
if (!rangeDisabled) defaultChecked()
|
||||||
|
|
||||||
|
onChange {
|
||||||
|
val checkBoxValue = it.target.checked
|
||||||
|
rangeDisabled = !checkBoxValue
|
||||||
|
onValueChange(
|
||||||
|
if (!checkBoxValue) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
innerValue?.asValue()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Input(type = InputType.Range) {
|
||||||
|
style {
|
||||||
|
width(100.percent)
|
||||||
|
}
|
||||||
|
if (rangeDisabled) disabled()
|
||||||
|
value(innerValue?.toString() ?: "")
|
||||||
|
onChange {
|
||||||
|
val newValue = it.target.value
|
||||||
|
onValueChange(newValue.toDoubleOrNull()?.asValue())
|
||||||
|
innerValue = newValue.toDoubleOrNull()
|
||||||
|
}
|
||||||
|
descriptor?.attributes?.get("min").string?.let {
|
||||||
|
min(it)
|
||||||
|
}
|
||||||
|
descriptor?.attributes?.get("max").string?.let {
|
||||||
|
max(it)
|
||||||
|
}
|
||||||
|
descriptor?.attributes?.get("step").number?.let {
|
||||||
|
step(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun ValueChooser(
|
||||||
|
descriptor: MetaDescriptor?,
|
||||||
|
state: EditorPropertyState,
|
||||||
|
value: Value?,
|
||||||
|
onValueChange: (Value?) -> Unit,
|
||||||
|
) {
|
||||||
|
val rawInput by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val type = descriptor?.valueTypes?.firstOrNull()
|
||||||
|
|
||||||
|
when {
|
||||||
|
rawInput -> StringValueChooser(descriptor, state, value, onValueChange)
|
||||||
|
descriptor?.widgetType == "color" -> ColorValueChooser(descriptor, state, value, onValueChange)
|
||||||
|
descriptor?.widgetType == "multiSelect" -> MultiSelectChooser(descriptor, state, value, onValueChange)
|
||||||
|
descriptor?.widgetType == "range" -> RangeValueChooser(descriptor, state, value, onValueChange)
|
||||||
|
type == ValueType.BOOLEAN -> BooleanValueChooser(descriptor, state, value, onValueChange)
|
||||||
|
type == ValueType.NUMBER -> NumberValueChooser(descriptor, state, value, onValueChange)
|
||||||
|
descriptor?.allowedValues?.isNotEmpty() ?: false -> ComboValueChooser(descriptor, state, value, onValueChange)
|
||||||
|
//TODO handle lists
|
||||||
|
else -> StringValueChooser(descriptor, state, value, onValueChange)
|
||||||
|
}
|
||||||
|
}
|
@ -16,9 +16,8 @@ import react.dom.attrs
|
|||||||
import space.kscience.dataforge.meta.MutableMeta
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
import space.kscience.dataforge.meta.ObservableMutableMeta
|
import space.kscience.dataforge.meta.ObservableMutableMeta
|
||||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
import space.kscience.dataforge.meta.descriptors.ValueRequirement
|
import space.kscience.dataforge.meta.descriptors.ValueRestriction
|
||||||
import space.kscience.dataforge.meta.descriptors.get
|
import space.kscience.dataforge.meta.descriptors.get
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.remove
|
import space.kscience.dataforge.meta.remove
|
||||||
import space.kscience.dataforge.names.*
|
import space.kscience.dataforge.names.*
|
||||||
import space.kscience.visionforge.hidden
|
import space.kscience.visionforge.hidden
|
||||||
@ -146,7 +145,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
|
|||||||
}
|
}
|
||||||
+token
|
+token
|
||||||
}
|
}
|
||||||
if (!props.name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) {
|
if (!props.name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) {
|
||||||
styledDiv {
|
styledDiv {
|
||||||
css {
|
css {
|
||||||
//+TreeStyles.resizeableInput
|
//+TreeStyles.resizeableInput
|
||||||
@ -185,7 +184,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
|
|||||||
}
|
}
|
||||||
+"\u00D7"
|
+"\u00D7"
|
||||||
attrs {
|
attrs {
|
||||||
if (editorPropertyState!= EditorPropertyState.Defined) {
|
if (editorPropertyState != EditorPropertyState.Defined) {
|
||||||
disabled = true
|
disabled = true
|
||||||
} else {
|
} else {
|
||||||
onClickFunction = removeClick
|
onClickFunction = removeClick
|
||||||
|
@ -12,7 +12,7 @@ import react.dom.attrs
|
|||||||
import react.fc
|
import react.fc
|
||||||
import react.useState
|
import react.useState
|
||||||
import space.kscience.dataforge.meta.asValue
|
import space.kscience.dataforge.meta.asValue
|
||||||
import space.kscience.dataforge.meta.descriptors.ValueRequirement
|
import space.kscience.dataforge.meta.descriptors.ValueRestriction
|
||||||
import space.kscience.dataforge.meta.double
|
import space.kscience.dataforge.meta.double
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
@ -43,7 +43,7 @@ public val RangeValueChooser: FC<ValueChooserProps> = fc("RangeValueChooser") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flexRow {
|
flexRow {
|
||||||
if (props.descriptor?.valueRequirement != ValueRequirement.REQUIRED) {
|
if (props.descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
|
||||||
styledInput(type = InputType.checkBox) {
|
styledInput(type = InputType.checkBox) {
|
||||||
attrs {
|
attrs {
|
||||||
defaultChecked = rangeDisabled.not()
|
defaultChecked = rangeDisabled.not()
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package space.kscience.visionforge
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.MetaRepr
|
||||||
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("control")
|
||||||
|
public abstract class VisionControlEvent : VisionEvent, MetaRepr {
|
||||||
|
public abstract val meta: Meta
|
||||||
|
|
||||||
|
override fun toMeta(): Meta = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ControlVision : Vision {
|
||||||
|
public val controlEventFlow: SharedFlow<VisionControlEvent>
|
||||||
|
|
||||||
|
public fun dispatchControlEvent(event: VisionControlEvent)
|
||||||
|
|
||||||
|
override fun receiveEvent(event: VisionEvent) {
|
||||||
|
if (event is VisionControlEvent) {
|
||||||
|
dispatchControlEvent(event)
|
||||||
|
} else super.receiveEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param payload The optional payload associated with the click event.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("control.click")
|
||||||
|
public class VisionClickEvent(public val payload: Meta = Meta.EMPTY) : VisionControlEvent() {
|
||||||
|
override val meta: Meta get() = Meta { ::payload.name put payload }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface ClickControl : ControlVision {
|
||||||
|
/**
|
||||||
|
* Create and dispatch a click event
|
||||||
|
*/
|
||||||
|
public fun click(builder: MutableMeta.() -> Unit = {}) {
|
||||||
|
dispatchControlEvent(VisionClickEvent(Meta(builder)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register listener
|
||||||
|
*/
|
||||||
|
public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job =
|
||||||
|
controlEventFlow.filterIsInstance<VisionClickEvent>().onEach(block).launchIn(scope)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("control.valueChange")
|
||||||
|
public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent()
|
@ -13,7 +13,7 @@ import kotlin.jvm.JvmInline
|
|||||||
@JvmInline
|
@JvmInline
|
||||||
public value class StyleSheet(private val owner: Vision) {
|
public value class StyleSheet(private val owner: Vision) {
|
||||||
|
|
||||||
private val styleNode: Meta get() = owner.properties.getMeta(STYLESHEET_KEY)
|
private val styleNode: Meta get() = owner.properties[STYLESHEET_KEY]
|
||||||
|
|
||||||
public val items: Map<NameToken, Meta> get() = styleNode.items
|
public val items: Map<NameToken, Meta> get() = styleNode.items
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ public value class StyleSheet(private val owner: Vision) {
|
|||||||
* Define a style without notifying owner
|
* Define a style without notifying owner
|
||||||
*/
|
*/
|
||||||
public fun define(key: String, style: Meta?) {
|
public fun define(key: String, style: Meta?) {
|
||||||
owner.properties.setMeta(STYLESHEET_KEY + key, style)
|
owner.properties[STYLESHEET_KEY + key] = style
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,7 +92,7 @@ public fun Vision.useStyle(name: String, notify: Boolean = true) {
|
|||||||
* Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision].
|
* Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision].
|
||||||
*/
|
*/
|
||||||
public fun Vision.getStyle(name: String): Meta? =
|
public fun Vision.getStyle(name: String): Meta? =
|
||||||
properties.own?.getMeta(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name)
|
properties.own?.get(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a property from all styles
|
* Resolve a property from all styles
|
||||||
|
@ -4,11 +4,13 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import space.kscience.dataforge.context.logger
|
||||||
|
import space.kscience.dataforge.context.warn
|
||||||
import space.kscience.dataforge.meta.asValue
|
import space.kscience.dataforge.meta.asValue
|
||||||
import space.kscience.dataforge.meta.boolean
|
import space.kscience.dataforge.meta.boolean
|
||||||
import space.kscience.dataforge.meta.descriptors.Described
|
import space.kscience.dataforge.meta.descriptors.Described
|
||||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
import space.kscience.dataforge.misc.Type
|
import space.kscience.dataforge.misc.DfType
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.asName
|
||||||
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
|
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
|
||||||
@ -17,7 +19,7 @@ import space.kscience.visionforge.Vision.Companion.TYPE
|
|||||||
/**
|
/**
|
||||||
* A root type for display hierarchy
|
* A root type for display hierarchy
|
||||||
*/
|
*/
|
||||||
@Type(TYPE)
|
@DfType(TYPE)
|
||||||
public interface Vision : Described {
|
public interface Vision : Described {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,6 +47,14 @@ public interface Vision : Described {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive and process a generic [VisionEvent].
|
||||||
|
*/
|
||||||
|
public fun receiveEvent(event: VisionEvent) {
|
||||||
|
if(event is VisionChange) update(event)
|
||||||
|
else manager?.logger?.warn { "Undispatched event: $event" }
|
||||||
|
}
|
||||||
|
|
||||||
override val descriptor: MetaDescriptor?
|
override val descriptor: MetaDescriptor?
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
|
@ -63,8 +63,7 @@ public data class VisionChange(
|
|||||||
public val vision: Vision? = null,
|
public val vision: Vision? = null,
|
||||||
public val properties: Meta? = null,
|
public val properties: Meta? = null,
|
||||||
public val children: Map<Name, VisionChange>? = null,
|
public val children: Map<Name, VisionChange>? = null,
|
||||||
)
|
) : VisionEvent
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An update for a [Vision]
|
* An update for a [Vision]
|
||||||
|
@ -13,12 +13,11 @@ import space.kscience.dataforge.names.parseAsName
|
|||||||
public interface VisionClient: Plugin {
|
public interface VisionClient: Plugin {
|
||||||
public val visionManager: VisionManager
|
public val visionManager: VisionManager
|
||||||
|
|
||||||
public suspend fun sendEvent(event: VisionEvent)
|
public suspend fun sendEvent(targetName: Name, event: VisionEvent)
|
||||||
|
|
||||||
public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?)
|
public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) {
|
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) {
|
||||||
notifyPropertyChanged(visionName, propertyName.parseAsName(true), item)
|
notifyPropertyChanged(visionName, propertyName.parseAsName(true), item)
|
||||||
}
|
}
|
||||||
@ -35,8 +34,8 @@ public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: St
|
|||||||
notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
|
notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun VisionClient.sendEvent(visionName: Name, event: MetaRepr): Unit {
|
public fun VisionClient.sendEvent(targetName: Name, payload: MetaRepr): Unit {
|
||||||
context.launch {
|
context.launch {
|
||||||
sendEvent(VisionMetaEvent(visionName, event.toMeta()))
|
sendEvent(targetName, VisionMetaEvent(payload.toMeta()))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,18 +3,14 @@ package space.kscience.visionforge
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MutableMeta
|
|
||||||
import space.kscience.dataforge.meta.set
|
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event propagated from client to a server
|
* An event propagated from client to a server
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
public sealed interface VisionEvent{
|
public sealed interface VisionEvent {
|
||||||
public val targetName: Name
|
public companion object {
|
||||||
|
|
||||||
public companion object{
|
|
||||||
public val CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
|
public val CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,17 +20,4 @@ public sealed interface VisionEvent{
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("meta")
|
@SerialName("meta")
|
||||||
public class VisionMetaEvent(override val targetName: Name, public val meta: Meta) : VisionEvent
|
public class VisionMetaEvent(public val meta: Meta) : VisionEvent
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@SerialName("change")
|
|
||||||
public class VisionChangeEvent(override val targetName: Name, public val change: VisionChange) : VisionEvent
|
|
||||||
|
|
||||||
public val Vision.Companion.CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the payload to be sent to server on click
|
|
||||||
*/
|
|
||||||
public fun Vision.onClickPayload(payloadBuilder: MutableMeta.() -> Unit){
|
|
||||||
properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder)
|
|
||||||
}
|
|
@ -13,10 +13,7 @@ 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.*
|
||||||
import space.kscience.visionforge.html.VisionOfHtmlForm
|
|
||||||
import space.kscience.visionforge.html.VisionOfNumberField
|
|
||||||
import space.kscience.visionforge.html.VisionOfTextField
|
|
||||||
|
|
||||||
public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionContainer<Vision> {
|
public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionContainer<Vision> {
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
@ -72,9 +69,11 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont
|
|||||||
defaultDeserializer { SimpleVisionGroup.serializer() }
|
defaultDeserializer { SimpleVisionGroup.serializer() }
|
||||||
subclass(NullVision.serializer())
|
subclass(NullVision.serializer())
|
||||||
subclass(SimpleVisionGroup.serializer())
|
subclass(SimpleVisionGroup.serializer())
|
||||||
|
subclass(VisionOfHtmlInput.serializer())
|
||||||
subclass(VisionOfNumberField.serializer())
|
subclass(VisionOfNumberField.serializer())
|
||||||
subclass(VisionOfTextField.serializer())
|
subclass(VisionOfTextField.serializer())
|
||||||
subclass(VisionOfCheckbox.serializer())
|
subclass(VisionOfCheckbox.serializer())
|
||||||
|
subclass(VisionOfRangeField.serializer())
|
||||||
subclass(VisionOfHtmlForm.serializer())
|
subclass(VisionOfHtmlForm.serializer())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,13 +34,13 @@ public interface VisionProperties : MetaProvider {
|
|||||||
* @param inherit toggles parent node property lookup. Null means inference from descriptor.
|
* @param inherit toggles parent node property lookup. Null means inference from descriptor.
|
||||||
* @param includeStyles toggles inclusion of properties from styles.
|
* @param includeStyles toggles inclusion of properties from styles.
|
||||||
*/
|
*/
|
||||||
public fun getMeta(
|
public fun get(
|
||||||
name: Name,
|
name: Name,
|
||||||
inherit: Boolean?,
|
inherit: Boolean?,
|
||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): Meta
|
): Meta
|
||||||
|
|
||||||
override fun getMeta(name: Name): Meta? = getMeta(name, null, null)
|
override fun get(name: Name): Meta? = get(name, null, null)
|
||||||
|
|
||||||
|
|
||||||
public val changes: Flow<Name>
|
public val changes: Flow<Name>
|
||||||
@ -54,7 +54,7 @@ public interface VisionProperties : MetaProvider {
|
|||||||
|
|
||||||
public interface MutableVisionProperties : VisionProperties, MutableMetaProvider {
|
public interface MutableVisionProperties : VisionProperties, MutableMetaProvider {
|
||||||
|
|
||||||
override fun getMeta(
|
override fun get(
|
||||||
name: Name,
|
name: Name,
|
||||||
inherit: Boolean?,
|
inherit: Boolean?,
|
||||||
includeStyles: Boolean?,
|
includeStyles: Boolean?,
|
||||||
@ -65,7 +65,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
|
|||||||
includeStyles,
|
includeStyles,
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun setMeta(
|
public fun set(
|
||||||
name: Name,
|
name: Name,
|
||||||
node: Meta?,
|
node: Meta?,
|
||||||
notify: Boolean,
|
notify: Boolean,
|
||||||
@ -77,10 +77,10 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
|
|||||||
notify: Boolean,
|
notify: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getMeta(name: Name): MutableMeta = getMeta(name, null, null)
|
override fun get(name: Name): MutableMeta = get(name, null, null)
|
||||||
|
|
||||||
override fun setMeta(name: Name, node: Meta?) {
|
override fun set(name: Name, node: Meta?) {
|
||||||
setMeta(name, node, true)
|
set(name, node, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setValue(name: Name, value: Value?) {
|
override fun setValue(name: Name, value: Value?) {
|
||||||
@ -89,7 +89,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun MutableVisionProperties.remove(name: Name) {
|
public fun MutableVisionProperties.remove(name: Name) {
|
||||||
setMeta(name, null)
|
set(name, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun MutableVisionProperties.remove(name: String) {
|
public fun MutableVisionProperties.remove(name: String) {
|
||||||
@ -114,7 +114,7 @@ private class VisionPropertiesItem(
|
|||||||
|
|
||||||
override val items: Map<NameToken, MutableMeta>
|
override val items: Map<NameToken, MutableMeta>
|
||||||
get() {
|
get() {
|
||||||
val metaKeys = properties.own?.getMeta(nodeName)?.items?.keys ?: emptySet()
|
val metaKeys = properties.own?.get(nodeName)?.items?.keys ?: emptySet()
|
||||||
val descriptorKeys = descriptor?.children?.map { NameToken(it.key) } ?: emptySet()
|
val descriptorKeys = descriptor?.children?.map { NameToken(it.key) } ?: emptySet()
|
||||||
val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet()
|
val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet()
|
||||||
val inheritFlag = descriptor?.inherited ?: inherit
|
val inheritFlag = descriptor?.inherited ?: inherit
|
||||||
@ -148,8 +148,8 @@ private class VisionPropertiesItem(
|
|||||||
default
|
default
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun setMeta(name: Name, node: Meta?) {
|
override fun set(name: Name, node: Meta?) {
|
||||||
properties.setMeta(nodeName + name, node)
|
properties[nodeName + name] = node
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = Meta.toString(this)
|
override fun toString(): String = Meta.toString(this)
|
||||||
@ -202,16 +202,16 @@ public abstract class AbstractVisionProperties(
|
|||||||
return descriptor?.defaultValue
|
return descriptor?.defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
|
override fun set(name: Name, node: Meta?, notify: Boolean) {
|
||||||
//ignore if the value is the same as existing
|
//ignore if the value is the same as existing
|
||||||
if (own?.getMeta(name) == node) return
|
if (own?.get(name) == node) return
|
||||||
|
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
properties = node?.asMutableMeta()
|
properties = node?.asMutableMeta()
|
||||||
} else if (node == null) {
|
} else if (node == null) {
|
||||||
properties?.setMeta(name, node)
|
properties?.set(name, node)
|
||||||
} else {
|
} else {
|
||||||
getOrCreateProperties().setMeta(name, node)
|
getOrCreateProperties()[name] = node
|
||||||
}
|
}
|
||||||
if (notify) {
|
if (notify) {
|
||||||
invalidate(name)
|
invalidate(name)
|
||||||
@ -223,7 +223,7 @@ public abstract class AbstractVisionProperties(
|
|||||||
if (own?.getValue(name) == value) return
|
if (own?.getValue(name) == value) return
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
properties?.getMeta(name)?.value = null
|
properties?.get(name)?.value = null
|
||||||
} else {
|
} else {
|
||||||
getOrCreateProperties().setValue(name, value)
|
getOrCreateProperties().setValue(name, value)
|
||||||
}
|
}
|
||||||
@ -272,11 +272,11 @@ public fun VisionProperties.getValue(
|
|||||||
/**
|
/**
|
||||||
* Get [Vision] property using key as a String
|
* Get [Vision] property using key as a String
|
||||||
*/
|
*/
|
||||||
public fun VisionProperties.getMeta(
|
public fun VisionProperties.get(
|
||||||
name: String,
|
name: String,
|
||||||
inherit: Boolean? = null,
|
inherit: Boolean? = null,
|
||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): Meta = getMeta(name.parseAsName(), inherit, includeStyles)
|
): Meta = get(name.parseAsName(), inherit, includeStyles)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The root property node with given inheritance and style flags
|
* The root property node with given inheritance and style flags
|
||||||
@ -286,17 +286,17 @@ public fun VisionProperties.getMeta(
|
|||||||
public fun MutableVisionProperties.root(
|
public fun MutableVisionProperties.root(
|
||||||
inherit: Boolean? = null,
|
inherit: Boolean? = null,
|
||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): MutableMeta = getMeta(Name.EMPTY, inherit, includeStyles)
|
): MutableMeta = get(Name.EMPTY, inherit, includeStyles)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get [Vision] property using key as a String
|
* Get [Vision] property using key as a String
|
||||||
*/
|
*/
|
||||||
public fun MutableVisionProperties.getMeta(
|
public fun MutableVisionProperties.get(
|
||||||
name: String,
|
name: String,
|
||||||
inherit: Boolean? = null,
|
inherit: Boolean? = null,
|
||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): MutableMeta = getMeta(name.parseAsName(), inherit, includeStyles)
|
): MutableMeta = get(name.parseAsName(), inherit, includeStyles)
|
||||||
|
|
||||||
//
|
//
|
||||||
//public operator fun MutableVisionProperties.set(name: Name, value: Number): Unit =
|
//public operator fun MutableVisionProperties.set(name: Name, value: Number): Unit =
|
||||||
|
@ -17,10 +17,10 @@ public fun Vision.flowProperty(
|
|||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): Flow<Meta> = flow {
|
): Flow<Meta> = flow {
|
||||||
//Pass initial value.
|
//Pass initial value.
|
||||||
emit(properties.getMeta(propertyName, inherit, includeStyles))
|
emit(properties.get(propertyName, inherit, includeStyles))
|
||||||
properties.changes.collect { name ->
|
properties.changes.collect { name ->
|
||||||
if (name.startsWith(propertyName)) {
|
if (name.startsWith(propertyName)) {
|
||||||
emit(properties.getMeta(propertyName, inherit, includeStyles))
|
emit(properties.get(propertyName, inherit, includeStyles))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
package space.kscience.visionforge.html
|
||||||
|
|
||||||
|
import kotlinx.html.InputType
|
||||||
|
import kotlinx.html.TagConsumer
|
||||||
|
import kotlinx.html.stream.createHTML
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import space.kscience.dataforge.meta.*
|
||||||
|
import space.kscience.dataforge.names.asName
|
||||||
|
import space.kscience.visionforge.AbstractVision
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public abstract class VisionOfHtml : AbstractVision() {
|
||||||
|
public var classes: List<String> by properties.stringList(*emptyArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.plain")
|
||||||
|
public class VisionOfPlainHtml : VisionOfHtml() {
|
||||||
|
public var content: String? by properties.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) {
|
||||||
|
content = createHTML().apply(block).finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.html(
|
||||||
|
block: VisionOfPlainHtml.() -> Unit,
|
||||||
|
): VisionOfPlainHtml = VisionOfPlainHtml().apply(block)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public enum class InputFeedbackMode {
|
||||||
|
/**
|
||||||
|
* Fire feedback event on `onchange` event
|
||||||
|
*/
|
||||||
|
ONCHANGE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire feedback event on `oninput` event
|
||||||
|
*/
|
||||||
|
ONINPUT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* provide only manual feedback
|
||||||
|
*/
|
||||||
|
NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.input")
|
||||||
|
public open class VisionOfHtmlInput(
|
||||||
|
public val inputType: String,
|
||||||
|
public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE,
|
||||||
|
) : VisionOfHtml() {
|
||||||
|
public var value: Value? by properties.value()
|
||||||
|
public var disabled: Boolean by properties.boolean { false }
|
||||||
|
public var fieldName: String? by properties.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlInput(
|
||||||
|
inputType: String,
|
||||||
|
block: VisionOfHtmlInput.() -> Unit = {},
|
||||||
|
): VisionOfHtmlInput = VisionOfHtmlInput(inputType).apply(block)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.text")
|
||||||
|
public class VisionOfTextField : VisionOfHtmlInput(InputType.text.realValue) {
|
||||||
|
public var text: String? by properties.string(key = VisionOfHtmlInput::value.name.asName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlTextField(
|
||||||
|
block: VisionOfTextField.() -> Unit = {},
|
||||||
|
): VisionOfTextField = VisionOfTextField().apply(block)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.checkbox")
|
||||||
|
public class VisionOfCheckbox : VisionOfHtmlInput(InputType.checkBox.realValue) {
|
||||||
|
public var checked: Boolean? by properties.boolean(key = VisionOfHtmlInput::value.name.asName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlCheckBox(
|
||||||
|
block: VisionOfCheckbox.() -> Unit = {},
|
||||||
|
): VisionOfCheckbox = VisionOfCheckbox().apply(block)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.number")
|
||||||
|
public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) {
|
||||||
|
public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlNumberField(
|
||||||
|
block: VisionOfNumberField.() -> Unit = {},
|
||||||
|
): VisionOfNumberField = VisionOfNumberField().apply(block)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.range")
|
||||||
|
public class VisionOfRangeField(
|
||||||
|
public val min: Double,
|
||||||
|
public val max: Double,
|
||||||
|
public val step: Double = 1.0,
|
||||||
|
) : VisionOfHtmlInput(InputType.range.realValue) {
|
||||||
|
public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlRangeField(
|
||||||
|
min: Double,
|
||||||
|
max: Double,
|
||||||
|
step: Double = 1.0,
|
||||||
|
block: VisionOfRangeField.() -> Unit = {},
|
||||||
|
): VisionOfRangeField = VisionOfRangeField(min, max, step).apply(block)
|
||||||
|
|
@ -9,12 +9,15 @@ import kotlinx.serialization.Serializable
|
|||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.node
|
import space.kscience.dataforge.meta.node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param formId an id of the element in rendered DOM, this form is bound to
|
||||||
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("html.form")
|
@SerialName("html.form")
|
||||||
public class VisionOfHtmlForm(
|
public class VisionOfHtmlForm(
|
||||||
public val formId: String,
|
public val formId: String,
|
||||||
) : VisionOfHtmlInput() {
|
) : VisionOfHtml() {
|
||||||
public var values: Meta? by mutableProperties.node()
|
public var values: Meta? by properties.node()
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <R> TagConsumer<R>.bindForm(
|
public fun <R> TagConsumer<R>.bindForm(
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
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.dataforge.names.Name
|
|
||||||
import space.kscience.visionforge.AbstractVision
|
|
||||||
import space.kscience.visionforge.Vision
|
|
||||||
|
|
||||||
//TODO replace by something
|
|
||||||
internal val Vision.mutableProperties get() = properties.getMeta(Name.EMPTY, false, false)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
public abstract class VisionOfHtmlInput : AbstractVision() {
|
|
||||||
public var disabled: Boolean by mutableProperties.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 mutableProperties.string()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@SerialName("html.checkbox")
|
|
||||||
public class VisionOfCheckbox(
|
|
||||||
public val label: String? = null,
|
|
||||||
public val name: String? = null,
|
|
||||||
) : VisionOfHtmlInput() {
|
|
||||||
public var checked: Boolean? by mutableProperties.boolean()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@SerialName("html.number")
|
|
||||||
public class VisionOfNumberField(
|
|
||||||
public val label: String? = null,
|
|
||||||
public val name: String? = null,
|
|
||||||
) : VisionOfHtmlInput() {
|
|
||||||
public var value: Number? by mutableProperties.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 mutableProperties.number()
|
|
||||||
}
|
|
||||||
|
|
@ -23,10 +23,10 @@ public fun Vision.useProperty(
|
|||||||
callback: (Meta) -> Unit,
|
callback: (Meta) -> Unit,
|
||||||
): Job {
|
): Job {
|
||||||
//Pass initial value.
|
//Pass initial value.
|
||||||
callback(properties.getMeta(propertyName, inherit, includeStyles))
|
callback(properties.get(propertyName, inherit, includeStyles))
|
||||||
return properties.changes.onEach { name ->
|
return properties.changes.onEach { name ->
|
||||||
if (name.startsWith(propertyName)) {
|
if (name.startsWith(propertyName)) {
|
||||||
callback(properties.getMeta(propertyName, inherit, includeStyles))
|
callback(properties.get(propertyName, inherit, includeStyles))
|
||||||
}
|
}
|
||||||
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
|
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
|
||||||
}
|
}
|
||||||
@ -39,9 +39,22 @@ public fun Vision.useProperty(
|
|||||||
callback: (Meta) -> Unit,
|
callback: (Meta) -> Unit,
|
||||||
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)
|
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe changes to the specific property without passing the initial value.
|
||||||
|
*/
|
||||||
|
public fun <V : Vision, T> V.onPropertyChange(
|
||||||
|
property: KProperty1<V, T>,
|
||||||
|
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
|
||||||
|
callback: suspend V.(T) -> Unit,
|
||||||
|
): Job = properties.changes.onEach { name ->
|
||||||
|
if (name.startsWith(property.name.asName())) {
|
||||||
|
callback(property.get(this))
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
public fun <V : Vision, T> V.useProperty(
|
public fun <V : Vision, T> V.useProperty(
|
||||||
property: KProperty1<V, T>,
|
property: KProperty1<V, T>,
|
||||||
scope: CoroutineScope? = manager?.context,
|
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
|
||||||
callback: V.(T) -> Unit,
|
callback: V.(T) -> Unit,
|
||||||
): Job {
|
): Job {
|
||||||
//Pass initial value.
|
//Pass initial value.
|
||||||
@ -50,5 +63,5 @@ public fun <V : Vision, T> V.useProperty(
|
|||||||
if (name.startsWith(property.name.asName())) {
|
if (name.startsWith(property.name.asName())) {
|
||||||
callback(property.get(this@useProperty))
|
callback(property.get(this@useProperty))
|
||||||
}
|
}
|
||||||
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
|
}.launchIn(scope)
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package space.kscience.visionforge.meta
|
package space.kscience.visionforge.meta
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.take
|
||||||
@ -24,7 +23,6 @@ private class TestScheme : Scheme() {
|
|||||||
companion object : SchemeSpec<TestScheme>(::TestScheme)
|
companion object : SchemeSpec<TestScheme>(::TestScheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
internal class VisionPropertyTest {
|
internal class VisionPropertyTest {
|
||||||
|
|
||||||
private val manager = Global.request(VisionManager)
|
private val manager = Global.request(VisionManager)
|
||||||
@ -42,7 +40,7 @@ internal class VisionPropertyTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testPropertyEdit() {
|
fun testPropertyEdit() {
|
||||||
val vision = manager.group()
|
val vision = manager.group()
|
||||||
vision.properties.getMeta("fff.ddd").apply {
|
vision.properties.get("fff.ddd").apply {
|
||||||
value = 2.asValue()
|
value = 2.asValue()
|
||||||
}
|
}
|
||||||
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
|
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
|
||||||
@ -52,7 +50,7 @@ internal class VisionPropertyTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testPropertyUpdate() {
|
fun testPropertyUpdate() {
|
||||||
val vision = manager.group()
|
val vision = manager.group()
|
||||||
vision.properties.getMeta("fff").updateWith(TestScheme) {
|
vision.properties.get("fff").updateWith(TestScheme) {
|
||||||
ddd = 2
|
ddd = 2
|
||||||
}
|
}
|
||||||
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
|
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
|
||||||
@ -87,7 +85,7 @@ internal class VisionPropertyTest {
|
|||||||
|
|
||||||
child.properties.remove("test")
|
child.properties.remove("test")
|
||||||
|
|
||||||
assertEquals(11, child.properties.getMeta("test", inherit = true).int)
|
assertEquals(11, child.properties.get("test", inherit = true).int)
|
||||||
// assertEquals(11, deferred.await()?.int)
|
// assertEquals(11, deferred.await()?.int)
|
||||||
// assertEquals(2, callCounter)
|
// assertEquals(2, callCounter)
|
||||||
subscription.cancel()
|
subscription.cancel()
|
||||||
|
@ -9,8 +9,8 @@ import kotlinx.serialization.serializerOrNull
|
|||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import org.w3c.dom.HTMLElement
|
import org.w3c.dom.HTMLElement
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.misc.DfType
|
||||||
import space.kscience.dataforge.misc.Named
|
import space.kscience.dataforge.misc.Named
|
||||||
import space.kscience.dataforge.misc.Type
|
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.asName
|
||||||
import space.kscience.dataforge.names.parseAsName
|
import space.kscience.dataforge.names.parseAsName
|
||||||
@ -20,13 +20,13 @@ import kotlin.reflect.cast
|
|||||||
/**
|
/**
|
||||||
* A browser renderer for a [Vision].
|
* A browser renderer for a [Vision].
|
||||||
*/
|
*/
|
||||||
@Type(ElementVisionRenderer.TYPE)
|
@DfType(ElementVisionRenderer.TYPE)
|
||||||
public interface ElementVisionRenderer : Named {
|
public interface ElementVisionRenderer : Named {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
|
* 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
|
* 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
|
* higher value to "steal" rendering job
|
||||||
*/
|
*/
|
||||||
public fun rateVision(vision: Vision): Int
|
public fun rateVision(vision: Vision): Int
|
||||||
|
|
||||||
|
@ -67,7 +67,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val changeCollector = VisionChangeBuilder()
|
|
||||||
|
private val rootChangeCollector = VisionChangeBuilder()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Communicate vision property changed from rendering engine to model
|
* Communicate vision property changed from rendering engine to model
|
||||||
@ -75,21 +76,20 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
override fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) {
|
override fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) {
|
||||||
context.launch {
|
context.launch {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
changeCollector.propertyChanged(visionName, propertyName, item)
|
rootChangeCollector.propertyChanged(visionName, propertyName, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val eventCollector by lazy {
|
private val eventCollector by lazy {
|
||||||
MutableSharedFlow<VisionEvent>(meta["feedback.eventCache"].int ?: 100)
|
MutableSharedFlow<Pair<Name, VisionEvent>>(meta["feedback.eventCache"].int ?: 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a custom feedback event
|
* Send a custom feedback event
|
||||||
*/
|
*/
|
||||||
override suspend fun sendEvent(event: VisionEvent) {
|
override suspend fun sendEvent(targetName: Name, event: VisionEvent) {
|
||||||
eventCollector.emit(event)
|
eventCollector.emit(targetName to event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderVision(element: Element, name: Name, vision: Vision, outputMeta: Meta) {
|
private fun renderVision(element: Element, name: Name, vision: Vision, outputMeta: Meta) {
|
||||||
@ -98,7 +98,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
renderer.render(element, name, vision, outputMeta)
|
renderer.render(element, name, vision, outputMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startVisionUpdate(element: Element, name: Name, vision: Vision?, outputMeta: Meta) {
|
private fun startVisionUpdate(element: Element, visionName: Name, vision: Vision, outputMeta: Meta) {
|
||||||
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
|
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
|
||||||
val wsUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) {
|
val wsUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) {
|
||||||
val endpoint = resolveEndpoint(element)
|
val endpoint = resolveEndpoint(element)
|
||||||
@ -110,9 +110,10 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
URL(attr.value)
|
URL(attr.value)
|
||||||
}.apply {
|
}.apply {
|
||||||
protocol = "ws"
|
protocol = "ws"
|
||||||
searchParams.append("name", name.toString())
|
searchParams.append("name", visionName.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
logger.info { "Updating vision data from $wsUrl" }
|
logger.info { "Updating vision data from $wsUrl" }
|
||||||
|
|
||||||
//Individual websocket for this vision
|
//Individual websocket for this vision
|
||||||
@ -120,25 +121,25 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
onmessage = { messageEvent ->
|
onmessage = { messageEvent ->
|
||||||
val stringData: String? = messageEvent.data as? String
|
val stringData: String? = messageEvent.data as? String
|
||||||
if (stringData != null) {
|
if (stringData != null) {
|
||||||
val change: VisionChange = visionManager.jsonFormat.decodeFromString(
|
val event: VisionEvent = visionManager.jsonFormat.decodeFromString(
|
||||||
VisionChange.serializer(),
|
VisionEvent.serializer(),
|
||||||
stringData
|
stringData
|
||||||
)
|
)
|
||||||
|
|
||||||
// If change contains root vision replacement, do it
|
// If change contains root vision replacement, do it
|
||||||
change.vision?.let { vision ->
|
if (event is VisionChange) {
|
||||||
renderVision(element, name, vision, outputMeta)
|
event.vision?.let { vision ->
|
||||||
|
renderVision(element, visionName, vision, outputMeta)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug { "Got update $change for output with name $name" }
|
logger.debug { "Got $event for output with name $visionName" }
|
||||||
if (vision == null) error("Can't update vision because it is not loaded.")
|
vision.receiveEvent(event)
|
||||||
vision.update(change)
|
|
||||||
} else {
|
} else {
|
||||||
logger.error { "WebSocket message data is not a string" }
|
logger.error { "WebSocket message data is not a string" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Backward change propagation
|
//Backward change propagation
|
||||||
var feedbackJob: Job? = null
|
var feedbackJob: Job? = null
|
||||||
|
|
||||||
@ -146,32 +147,35 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
val feedbackAggregationTime = meta["feedback.aggregationTime"]?.int ?: 300
|
val feedbackAggregationTime = meta["feedback.aggregationTime"]?.int ?: 300
|
||||||
|
|
||||||
onopen = {
|
onopen = {
|
||||||
|
|
||||||
feedbackJob = visionManager.context.launch {
|
feedbackJob = visionManager.context.launch {
|
||||||
eventCollector.filter { it.targetName == name }.onEach {
|
//launch a separate coroutine to send events to the backend
|
||||||
send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it))
|
eventCollector.filter { it.first == visionName }.onEach {
|
||||||
|
send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it.second))
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
|
//aggregate atomic changes
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
delay(feedbackAggregationTime.milliseconds)
|
delay(feedbackAggregationTime.milliseconds)
|
||||||
val change = changeCollector[name] ?: continue
|
val visionChangeCollector = rootChangeCollector[name]
|
||||||
if (!change.isEmpty()) {
|
if (visionChangeCollector?.isEmpty() == false) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
eventCollector.emit(VisionChangeEvent(name, change.deepCopy(visionManager)))
|
eventCollector.emit(visionName to visionChangeCollector.deepCopy(visionManager))
|
||||||
change.reset()
|
rootChangeCollector.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info { "WebSocket feedback channel established for output '$name'" }
|
logger.info { "WebSocket feedback channel established for output '$visionName'" }
|
||||||
}
|
}
|
||||||
|
|
||||||
onclose = {
|
onclose = {
|
||||||
feedbackJob?.cancel()
|
feedbackJob?.cancel()
|
||||||
logger.info { "WebSocket feedback channel closed for output '$name'" }
|
logger.info { "WebSocket feedback channel closed for output '$visionName'" }
|
||||||
}
|
}
|
||||||
onerror = {
|
onerror = {
|
||||||
feedbackJob?.cancel()
|
feedbackJob?.cancel()
|
||||||
logger.error { "WebSocket feedback channel error for output '$name'" }
|
logger.error { "WebSocket feedback channel error for output '$visionName'" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,9 +244,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Try to load vision via websocket
|
//Try to load vision via websocket
|
||||||
element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> {
|
// element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> {
|
||||||
startVisionUpdate(element, name, null, outputMeta)
|
// startVisionUpdate(element, name, null, outputMeta)
|
||||||
}
|
// }
|
||||||
|
|
||||||
else -> error("No embedded vision data / fetch url for $name")
|
else -> error("No embedded vision data / fetch url for $name")
|
||||||
}
|
}
|
||||||
@ -251,9 +255,13 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) {
|
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) {
|
||||||
listOf(
|
listOf(
|
||||||
numberVisionRenderer(this),
|
htmlVisionRenderer,
|
||||||
textVisionRenderer(this),
|
inputVisionRenderer,
|
||||||
formVisionRenderer(this)
|
checkboxVisionRenderer,
|
||||||
|
numberVisionRenderer,
|
||||||
|
textVisionRenderer,
|
||||||
|
rangeVisionRenderer,
|
||||||
|
formVisionRenderer
|
||||||
).associateByName()
|
).associateByName()
|
||||||
} else super<AbstractPlugin>.content(target)
|
} else super<AbstractPlugin>.content(target)
|
||||||
|
|
||||||
|
@ -2,67 +2,174 @@ package space.kscience.visionforge
|
|||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import kotlinx.html.InputType
|
import kotlinx.html.InputType
|
||||||
|
import kotlinx.html.div
|
||||||
import kotlinx.html.js.input
|
import kotlinx.html.js.input
|
||||||
import kotlinx.html.js.label
|
import org.w3c.dom.HTMLElement
|
||||||
import kotlinx.html.js.onChangeFunction
|
|
||||||
import org.w3c.dom.HTMLFormElement
|
import org.w3c.dom.HTMLFormElement
|
||||||
import org.w3c.dom.HTMLInputElement
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
import org.w3c.dom.get
|
import org.w3c.dom.get
|
||||||
import org.w3c.xhr.FormData
|
import org.w3c.xhr.FormData
|
||||||
import space.kscience.dataforge.context.debug
|
import space.kscience.dataforge.context.debug
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.meta.DynamicMeta
|
import space.kscience.dataforge.meta.*
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.visionforge.html.*
|
||||||
import space.kscience.dataforge.meta.toMap
|
|
||||||
import space.kscience.dataforge.meta.valueSequence
|
|
||||||
import space.kscience.visionforge.html.VisionOfHtmlForm
|
|
||||||
import space.kscience.visionforge.html.VisionOfNumberField
|
|
||||||
import space.kscience.visionforge.html.VisionOfTextField
|
|
||||||
|
|
||||||
internal fun textVisionRenderer(
|
/**
|
||||||
client: JsVisionClient,
|
* Subscribes the HTML element to a given vision.
|
||||||
): ElementVisionRenderer = ElementVisionRenderer<VisionOfTextField> { name, vision, _ ->
|
*
|
||||||
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
|
* @param vision The vision to subscribe to.
|
||||||
vision.label?.let {
|
*/
|
||||||
label {
|
private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
|
||||||
htmlFor = fieldName
|
vision.useProperty(VisionOfHtml::classes) {
|
||||||
+it
|
classList.value = classes.joinToString(separator = " ")
|
||||||
}
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
type = InputType.text
|
|
||||||
this.name = fieldName
|
|
||||||
vision.useProperty(VisionOfTextField::text) {
|
|
||||||
value = it ?: ""
|
|
||||||
}
|
|
||||||
onChangeFunction = {
|
|
||||||
client.notifyPropertyChanged(name, VisionOfTextField::text.name, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun numberVisionRenderer(
|
|
||||||
client: JsVisionClient,
|
/**
|
||||||
): ElementVisionRenderer = ElementVisionRenderer<VisionOfNumberField> { name, vision, _ ->
|
* Subscribes the HTML input element to a given vision.
|
||||||
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
|
*
|
||||||
vision.label?.let {
|
* @param inputVision The input vision to subscribe to.
|
||||||
label {
|
*/
|
||||||
htmlFor = fieldName
|
private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
|
||||||
+it
|
subscribeToVision(inputVision)
|
||||||
}
|
inputVision.useProperty(VisionOfHtmlInput::disabled) {
|
||||||
}
|
disabled = it
|
||||||
input {
|
|
||||||
type = InputType.text
|
|
||||||
this.name = fieldName
|
|
||||||
vision.useProperty(VisionOfNumberField::value) {
|
|
||||||
value = it?.toDouble() ?: 0.0
|
|
||||||
}
|
|
||||||
onChangeFunction = {
|
|
||||||
client.notifyPropertyChanged(name, VisionOfNumberField::value.name, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val htmlVisionRenderer: ElementVisionRenderer =
|
||||||
|
ElementVisionRenderer<VisionOfPlainHtml> { _, vision, _ ->
|
||||||
|
div {}.also { div ->
|
||||||
|
div.subscribeToVision(vision)
|
||||||
|
vision.useProperty(VisionOfPlainHtml::content) {
|
||||||
|
div.textContent = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val inputVisionRenderer: ElementVisionRenderer =
|
||||||
|
ElementVisionRenderer<VisionOfHtmlInput>(acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1) { _, vision, _ ->
|
||||||
|
input {
|
||||||
|
type = InputType.text
|
||||||
|
}.also { htmlInputElement ->
|
||||||
|
val onEvent: (Event) -> Unit = {
|
||||||
|
vision.value = htmlInputElement.value.asValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
when (vision.feedbackMode) {
|
||||||
|
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
|
||||||
|
|
||||||
|
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
|
||||||
|
InputFeedbackMode.NONE -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlInputElement.subscribeToInput(vision)
|
||||||
|
vision.useProperty(VisionOfHtmlInput::value) {
|
||||||
|
htmlInputElement.value = it?.string ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val checkboxVisionRenderer: ElementVisionRenderer =
|
||||||
|
ElementVisionRenderer<VisionOfCheckbox> { _, vision, _ ->
|
||||||
|
input {
|
||||||
|
type = InputType.checkBox
|
||||||
|
}.also { htmlInputElement ->
|
||||||
|
val onEvent: (Event) -> Unit = {
|
||||||
|
vision.checked = htmlInputElement.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
when (vision.feedbackMode) {
|
||||||
|
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
|
||||||
|
|
||||||
|
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
|
||||||
|
InputFeedbackMode.NONE -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlInputElement.subscribeToInput(vision)
|
||||||
|
vision.useProperty(VisionOfCheckbox::checked) {
|
||||||
|
htmlInputElement.checked = it ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val textVisionRenderer: ElementVisionRenderer =
|
||||||
|
ElementVisionRenderer<VisionOfTextField> { _, vision, _ ->
|
||||||
|
input {
|
||||||
|
type = InputType.text
|
||||||
|
}.also { htmlInputElement ->
|
||||||
|
val onEvent: (Event) -> Unit = {
|
||||||
|
vision.text = htmlInputElement.value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
when (vision.feedbackMode) {
|
||||||
|
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
|
||||||
|
|
||||||
|
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
|
||||||
|
InputFeedbackMode.NONE -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlInputElement.subscribeToInput(vision)
|
||||||
|
vision.useProperty(VisionOfTextField::text) {
|
||||||
|
htmlInputElement.value = it ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val numberVisionRenderer: ElementVisionRenderer =
|
||||||
|
ElementVisionRenderer<VisionOfNumberField> { _, vision, _ ->
|
||||||
|
input {
|
||||||
|
type = InputType.text
|
||||||
|
}.also { htmlInputElement ->
|
||||||
|
|
||||||
|
val onEvent: (Event) -> Unit = {
|
||||||
|
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
when (vision.feedbackMode) {
|
||||||
|
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
|
||||||
|
|
||||||
|
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
|
||||||
|
InputFeedbackMode.NONE -> {}
|
||||||
|
}
|
||||||
|
htmlInputElement.subscribeToInput(vision)
|
||||||
|
vision.useProperty(VisionOfNumberField::value) {
|
||||||
|
htmlInputElement.valueAsNumber = it?.double ?: 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val rangeVisionRenderer: ElementVisionRenderer =
|
||||||
|
ElementVisionRenderer<VisionOfRangeField> { _, vision, _ ->
|
||||||
|
input {
|
||||||
|
type = InputType.text
|
||||||
|
min = vision.min.toString()
|
||||||
|
max = vision.max.toString()
|
||||||
|
step = vision.step.toString()
|
||||||
|
}.also { htmlInputElement ->
|
||||||
|
|
||||||
|
val onEvent: (Event) -> Unit = {
|
||||||
|
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
when (vision.feedbackMode) {
|
||||||
|
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
|
||||||
|
|
||||||
|
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
|
||||||
|
InputFeedbackMode.NONE -> {}
|
||||||
|
}
|
||||||
|
htmlInputElement.subscribeToInput(vision)
|
||||||
|
vision.useProperty(VisionOfRangeField::value) {
|
||||||
|
htmlInputElement.valueAsNumber = it?.double ?: 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun FormData.toMeta(): Meta {
|
internal fun FormData.toMeta(): Meta {
|
||||||
@Suppress("UNUSED_VARIABLE") val formData = this
|
@Suppress("UNUSED_VARIABLE") val formData = this
|
||||||
//val res = js("Object.fromEntries(formData);")
|
//val res = js("Object.fromEntries(formData);")
|
||||||
@ -86,28 +193,29 @@ internal fun FormData.toMeta(): Meta {
|
|||||||
return DynamicMeta(`object`)
|
return DynamicMeta(`object`)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun formVisionRenderer(
|
internal val formVisionRenderer: ElementVisionRenderer =
|
||||||
client: JsVisionClient,
|
ElementVisionRenderer<VisionOfHtmlForm> { _, vision, _ ->
|
||||||
): ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlForm> { name, vision, _ ->
|
|
||||||
|
|
||||||
val form = document.getElementById(vision.formId) as? HTMLFormElement
|
val form = document.getElementById(vision.formId) as? HTMLFormElement
|
||||||
?: error("An element with id = '${vision.formId} is not a form")
|
?: error("An element with id = '${vision.formId} is not a form")
|
||||||
|
|
||||||
client.logger.debug{"Adding hooks to form with id = '$vision.formId'"}
|
form.subscribeToVision(vision)
|
||||||
|
|
||||||
vision.useProperty(VisionOfHtmlForm::values) { values ->
|
vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" }
|
||||||
client.logger.debug{"Updating form '${vision.formId}' with values $values"}
|
|
||||||
val inputs = form.getElementsByTagName("input")
|
vision.useProperty(VisionOfHtmlForm::values) { values ->
|
||||||
values?.valueSequence()?.forEach { (token, value) ->
|
vision.manager?.logger?.debug { "Updating form '${vision.formId}' with values $values" }
|
||||||
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
|
val inputs = form.getElementsByTagName("input")
|
||||||
|
values?.valueSequence()?.forEach { (token, value) ->
|
||||||
|
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
form.onsubmit = { event ->
|
form.onsubmit = { event ->
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
val formData = FormData(form).toMeta()
|
val formData = FormData(form).toMeta()
|
||||||
client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData)
|
vision.values = formData
|
||||||
console.info("Sent: ${formData.toMap()}")
|
console.info("Sent: ${formData.toMap()}")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,7 +7,6 @@ import org.jetbrains.kotlinx.jupyter.api.declare
|
|||||||
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
|
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
|
||||||
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.misc.DFExperimental
|
|
||||||
import space.kscience.visionforge.Vision
|
import space.kscience.visionforge.Vision
|
||||||
import space.kscience.visionforge.VisionManager
|
import space.kscience.visionforge.VisionManager
|
||||||
import space.kscience.visionforge.html.*
|
import space.kscience.visionforge.html.*
|
||||||
@ -17,7 +16,6 @@ import kotlin.random.nextUInt
|
|||||||
/**
|
/**
|
||||||
* A base class for different Jupyter VF integrations
|
* A base class for different Jupyter VF integrations
|
||||||
*/
|
*/
|
||||||
@DFExperimental
|
|
||||||
public abstract class VisionForgeIntegration(
|
public abstract class VisionForgeIntegration(
|
||||||
public val visionManager: VisionManager,
|
public val visionManager: VisionManager,
|
||||||
) : JupyterIntegration(), ContextAware {
|
) : JupyterIntegration(), ContextAware {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package space.kscience.visionforge.jupyter
|
package space.kscience.visionforge.jupyter
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.div
|
||||||
|
import kotlinx.html.p
|
||||||
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
|
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.gdml.Gdml
|
import space.kscience.gdml.Gdml
|
||||||
import space.kscience.plotly.Plot
|
import space.kscience.plotly.Plot
|
||||||
import space.kscience.plotly.PlotlyPage
|
import space.kscience.plotly.PlotlyPage
|
||||||
import space.kscience.plotly.StaticPlotlyRenderer
|
import space.kscience.plotly.StaticPlotlyRenderer
|
||||||
import space.kscience.tables.*
|
import space.kscience.tables.Table
|
||||||
import space.kscience.visionforge.gdml.toVision
|
import space.kscience.visionforge.gdml.toVision
|
||||||
import space.kscience.visionforge.html.HtmlFragment
|
import space.kscience.visionforge.html.HtmlFragment
|
||||||
import space.kscience.visionforge.html.VisionPage
|
import space.kscience.visionforge.html.VisionPage
|
||||||
@ -21,7 +21,6 @@ import space.kscience.visionforge.tables.toVision
|
|||||||
import space.kscience.visionforge.visionManager
|
import space.kscience.visionforge.visionManager
|
||||||
|
|
||||||
|
|
||||||
@DFExperimental
|
|
||||||
public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) {
|
public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) {
|
||||||
|
|
||||||
override fun Builder.afterLoaded(vf: VisionForge) {
|
override fun Builder.afterLoaded(vf: VisionForge) {
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
}
|
}
|
||||||
|
|
||||||
val plotlyVersion = "0.6.0"
|
val plotlyVersion = "0.6.1"
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
|
@ -33,8 +33,8 @@ public class VisionOfPlotly private constructor(
|
|||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
override val properties: MutableVisionProperties = object : MutableVisionProperties {
|
override val properties: MutableVisionProperties = object : MutableVisionProperties {
|
||||||
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
|
override fun set(name: Name, node: Meta?, notify: Boolean) {
|
||||||
meta.setMeta(name, node)
|
meta[name] = node
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setValue(name: Name, value: Value?, notify: Boolean) {
|
override fun setValue(name: Name, value: Value?, notify: Boolean) {
|
||||||
@ -45,11 +45,11 @@ public class VisionOfPlotly private constructor(
|
|||||||
|
|
||||||
override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor
|
override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor
|
||||||
|
|
||||||
override fun getMeta(
|
override fun get(
|
||||||
name: Name,
|
name: Name,
|
||||||
inherit: Boolean?,
|
inherit: Boolean?,
|
||||||
includeStyles: Boolean?,
|
includeStyles: Boolean?,
|
||||||
): MutableMeta = meta.getMeta(name) ?: MutableMeta()
|
): MutableMeta = meta[name] ?: MutableMeta()
|
||||||
|
|
||||||
override fun getValue(
|
override fun getValue(
|
||||||
name: Name,
|
name: Name,
|
||||||
|
@ -14,6 +14,7 @@ import io.ktor.server.util.*
|
|||||||
import io.ktor.server.websocket.*
|
import io.ktor.server.websocket.*
|
||||||
import io.ktor.util.pipeline.*
|
import io.ktor.util.pipeline.*
|
||||||
import io.ktor.websocket.*
|
import io.ktor.websocket.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -31,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
public class VisionRoute(
|
public class VisionRoute(
|
||||||
public val route: String,
|
public val route: String,
|
||||||
public val visionManager: VisionManager,
|
public val visionManager: VisionManager,
|
||||||
override val meta: ObservableMutableMeta = MutableMeta(),
|
override val meta: ObservableMutableMeta = ObservableMutableMeta(),
|
||||||
) : Configurable, ContextAware {
|
) : Configurable, ContextAware {
|
||||||
|
|
||||||
public enum class Mode {
|
public enum class Mode {
|
||||||
@ -71,14 +72,11 @@ public class VisionRoute(
|
|||||||
/**
|
/**
|
||||||
* Serve visions in a given [route] without providing a page template.
|
* Serve visions in a given [route] without providing a page template.
|
||||||
* [visions] could be changed during the service.
|
* [visions] could be changed during the service.
|
||||||
|
*
|
||||||
|
* @return a [Flow] of backward events, including vision change events
|
||||||
*/
|
*/
|
||||||
public fun Application.serveVisionData(
|
public fun Application.serveVisionData(
|
||||||
configuration: VisionRoute,
|
configuration: VisionRoute,
|
||||||
onEvent: suspend Vision.(VisionEvent) -> Unit = { event ->
|
|
||||||
if (event is VisionChangeEvent) {
|
|
||||||
update(event.change)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resolveVision: (Name) -> Vision?,
|
resolveVision: (Name) -> Vision?,
|
||||||
) {
|
) {
|
||||||
require(WebSockets)
|
require(WebSockets)
|
||||||
@ -102,16 +100,17 @@ public fun Application.serveVisionData(
|
|||||||
val event = configuration.visionManager.jsonFormat.decodeFromString(
|
val event = configuration.visionManager.jsonFormat.decodeFromString(
|
||||||
VisionEvent.serializer(), data
|
VisionEvent.serializer(), data
|
||||||
)
|
)
|
||||||
vision.onEvent(event)
|
|
||||||
|
vision.receiveEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
withContext(configuration.context.coroutineContext) {
|
withContext(configuration.context.coroutineContext) {
|
||||||
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update ->
|
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { event ->
|
||||||
val json = configuration.visionManager.jsonFormat.encodeToString(
|
val json = configuration.visionManager.jsonFormat.encodeToString(
|
||||||
VisionChange.serializer(),
|
VisionEvent.serializer(),
|
||||||
update
|
event
|
||||||
)
|
)
|
||||||
application.log.debug("Sending update for $name: \n$json")
|
application.log.debug("Sending update for $name: \n$json")
|
||||||
outgoing.send(Frame.Text(json))
|
outgoing.send(Frame.Text(json))
|
||||||
@ -147,6 +146,8 @@ public fun Application.serveVisionData(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Serve a page, potentially containing any number of visions at a given [route] with given [header].
|
* Serve a page, potentially containing any number of visions at a given [route] with given [header].
|
||||||
|
*
|
||||||
|
* @return a [Flow] containing backward propagated events, including vision change events
|
||||||
*/
|
*/
|
||||||
public fun Application.visionPage(
|
public fun Application.visionPage(
|
||||||
route: String,
|
route: String,
|
||||||
@ -154,7 +155,7 @@ public fun Application.visionPage(
|
|||||||
headers: Collection<HtmlFragment>,
|
headers: Collection<HtmlFragment>,
|
||||||
connector: EngineConnectorConfig? = null,
|
connector: EngineConnectorConfig? = null,
|
||||||
visionFragment: HtmlVisionFragment,
|
visionFragment: HtmlVisionFragment,
|
||||||
) {
|
){
|
||||||
require(WebSockets)
|
require(WebSockets)
|
||||||
|
|
||||||
val collector: MutableMap<Name, Vision> = mutableMapOf()
|
val collector: MutableMap<Name, Vision> = mutableMapOf()
|
||||||
|
@ -34,33 +34,33 @@ public fun Vision.colorProperty(
|
|||||||
ColorAccessor(properties.root(true), propertyName ?: property.name.asName())
|
ColorAccessor(properties.root(true), propertyName ?: property.name.asName())
|
||||||
}
|
}
|
||||||
|
|
||||||
public var ColorAccessor?.string: String?
|
public var ColorAccessor.string: String?
|
||||||
get() = this?.value?.let { if (it == Null) null else it.string }
|
get() = value?.let { if (it == Null) null else it.string }
|
||||||
set(value) {
|
set(value) {
|
||||||
this?.value = value?.asValue()
|
this.value = value?.asValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set [webcolor](https://en.wikipedia.org/wiki/Web_colors) as string
|
* Set [webcolor](https://en.wikipedia.org/wiki/Web_colors) as string
|
||||||
*/
|
*/
|
||||||
public operator fun ColorAccessor?.invoke(webColor: String) {
|
public operator fun ColorAccessor.invoke(webColor: String) {
|
||||||
this?.value = webColor.asValue()
|
value = webColor.asValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set color as RGB integer
|
* Set color as RGB integer
|
||||||
*/
|
*/
|
||||||
public operator fun ColorAccessor?.invoke(rgb: Int) {
|
public operator fun ColorAccessor.invoke(rgb: Int) {
|
||||||
this?.value = Colors.rgbToString(rgb).asValue()
|
value = Colors.rgbToString(rgb).asValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set color as RGB
|
* Set color as RGB
|
||||||
*/
|
*/
|
||||||
public operator fun ColorAccessor?.invoke(r: UByte, g: UByte, b: UByte) {
|
public operator fun ColorAccessor.invoke(r: UByte, g: UByte, b: UByte) {
|
||||||
this?.value = Colors.rgbToString(r, g, b).asValue()
|
value = Colors.rgbToString(r, g, b).asValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ColorAccessor?.clear() {
|
public fun ColorAccessor.clear() {
|
||||||
this?.value = null
|
value = null
|
||||||
}
|
}
|
@ -39,7 +39,7 @@ public inline fun MutableVisionContainer<Solid>.composite(
|
|||||||
}
|
}
|
||||||
val res = Composite(type, children[0], children[1])
|
val res = Composite(type, children[0], children[1])
|
||||||
|
|
||||||
res.properties.setMeta(Name.EMPTY, group.properties.own)
|
res.properties[Name.EMPTY] = group.properties.own
|
||||||
|
|
||||||
setChild(name, res)
|
setChild(name, res)
|
||||||
return res
|
return res
|
||||||
|
@ -92,7 +92,7 @@ public class Extruded(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun build(): Extruded = Extruded(shape, layers).apply {
|
internal fun build(): Extruded = Extruded(shape, layers).apply {
|
||||||
this.properties.setMeta(Name.EMPTY, this@Builder.properties)
|
this.properties[Name.EMPTY] = this@Builder.properties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ internal fun float32Vector(
|
|||||||
|
|
||||||
override fun setValue(thisRef: Solid, property: KProperty<*>, value: Float32Vector3D?) {
|
override fun setValue(thisRef: Solid, property: KProperty<*>, value: Float32Vector3D?) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
thisRef.properties.setMeta(name, null)
|
thisRef.properties[name] = null
|
||||||
} else {
|
} else {
|
||||||
thisRef.properties[name + X_KEY] = value.x
|
thisRef.properties[name + X_KEY] = value.x
|
||||||
thisRef.properties[name + Y_KEY] = value.y
|
thisRef.properties[name + Y_KEY] = value.y
|
||||||
|
@ -110,12 +110,12 @@ public val Solid.color: ColorAccessor
|
|||||||
get() = ColorAccessor(properties.root(true), MATERIAL_COLOR_KEY)
|
get() = ColorAccessor(properties.root(true), MATERIAL_COLOR_KEY)
|
||||||
|
|
||||||
public var Solid.material: SolidMaterial?
|
public var Solid.material: SolidMaterial?
|
||||||
get() = SolidMaterial.read(properties.getMeta(MATERIAL_KEY))
|
get() = SolidMaterial.read(properties[MATERIAL_KEY])
|
||||||
set(value) = properties.setMeta(MATERIAL_KEY, value?.meta)
|
set(value) = properties.set(MATERIAL_KEY, value?.meta)
|
||||||
|
|
||||||
@VisionBuilder
|
@VisionBuilder
|
||||||
public fun Solid.material(builder: SolidMaterial.() -> Unit) {
|
public fun Solid.material(builder: SolidMaterial.() -> Unit) {
|
||||||
properties.getMeta(MATERIAL_KEY).updateWith(SolidMaterial, builder)
|
properties[MATERIAL_KEY].updateWith(SolidMaterial, builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var Solid.opacity: Number?
|
public var Solid.opacity: Number?
|
||||||
@ -128,5 +128,5 @@ public var Solid.opacity: Number?
|
|||||||
@VisionBuilder
|
@VisionBuilder
|
||||||
public fun Solid.edges(enabled: Boolean = true, block: SolidMaterial.() -> Unit = {}) {
|
public fun Solid.edges(enabled: Boolean = true, block: SolidMaterial.() -> Unit = {}) {
|
||||||
properties[SolidMaterial.EDGES_ENABLED_KEY] = enabled
|
properties[SolidMaterial.EDGES_ENABLED_KEY] = enabled
|
||||||
SolidMaterial.write(properties.getMeta(SolidMaterial.EDGES_MATERIAL_KEY)).apply(block)
|
SolidMaterial.write(properties[SolidMaterial.EDGES_MATERIAL_KEY]).apply(block)
|
||||||
}
|
}
|
@ -162,7 +162,7 @@ internal class SolidReferenceChild(
|
|||||||
override val properties: MutableVisionProperties = object : MutableVisionProperties {
|
override val properties: MutableVisionProperties = object : MutableVisionProperties {
|
||||||
override val descriptor: MetaDescriptor get() = this@SolidReferenceChild.descriptor
|
override val descriptor: MetaDescriptor get() = this@SolidReferenceChild.descriptor
|
||||||
|
|
||||||
override val own: MutableMeta by lazy { owner.properties.getMeta(childToken(childName).asName()) }
|
override val own: MutableMeta by lazy { owner.properties[childToken(childName).asName()] }
|
||||||
|
|
||||||
override fun getValue(
|
override fun getValue(
|
||||||
name: Name,
|
name: Name,
|
||||||
@ -170,8 +170,8 @@ internal class SolidReferenceChild(
|
|||||||
includeStyles: Boolean?,
|
includeStyles: Boolean?,
|
||||||
): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles)
|
): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles)
|
||||||
|
|
||||||
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
|
override fun set(name: Name, node: Meta?, notify: Boolean) {
|
||||||
own.setMeta(name, node)
|
own[name] = node
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setValue(name: Name, value: Value?, notify: Boolean) {
|
override fun setValue(name: Name, value: Value?, notify: Boolean) {
|
||||||
|
@ -155,7 +155,7 @@ public class Surface(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun build(): Surface = Surface(layers).apply {
|
internal fun build(): Surface = Surface(layers).apply {
|
||||||
properties.setMeta(Name.EMPTY, this@Builder.properties)
|
properties[Name.EMPTY] = this@Builder.properties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,9 +36,9 @@ internal fun Meta.toVector2D(): Float32Vector2D =
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
internal fun MetaProvider.point3D(default: Float = 0f) = Float32Euclidean3DSpace.vector(
|
internal fun MetaProvider.point3D(default: Float = 0f) = Float32Euclidean3DSpace.vector(
|
||||||
getMeta(X_KEY).float ?: default,
|
get(X_KEY).float ?: default,
|
||||||
getMeta(Y_KEY).float ?: default,
|
get(Y_KEY).float ?: default,
|
||||||
getMeta(Z_KEY).float ?: default
|
get(Z_KEY).float ?: default
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ internal fun Solid.updateFrom(other: Solid): Solid {
|
|||||||
scaleX *= other.scaleX
|
scaleX *= other.scaleX
|
||||||
scaleY *= other.scaleY
|
scaleY *= other.scaleY
|
||||||
scaleZ *= other.scaleZ
|
scaleZ *= other.scaleZ
|
||||||
properties.setMeta(Name.EMPTY, other.properties.root())
|
properties[Name.EMPTY] = other.properties.root()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ class SolidPropertyTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertEquals("#555555", box?.color.string)
|
assertEquals("#555555", box?.color?.string)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -31,7 +31,7 @@ class SolidReferenceTest {
|
|||||||
fun testReferenceSerialization(){
|
fun testReferenceSerialization(){
|
||||||
val serialized = Solids.jsonForSolids.encodeToJsonElement(groupWithReference)
|
val serialized = Solids.jsonForSolids.encodeToJsonElement(groupWithReference)
|
||||||
val deserialized = Solids.jsonForSolids.decodeFromJsonElement(SolidGroup.serializer(), serialized)
|
val deserialized = Solids.jsonForSolids.decodeFromJsonElement(SolidGroup.serializer(), serialized)
|
||||||
assertEquals(groupWithReference.items["test"]?.color.string, deserialized.items["test"]?.color.string)
|
assertEquals(groupWithReference.items["test"]?.color?.string, deserialized.items["test"]?.color?.string)
|
||||||
assertEquals("blue", (deserialized.children.getChild("test") as Solid).color.string)
|
assertEquals("blue", (deserialized.children.getChild("test") as Solid).color.string)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
}
|
}
|
||||||
|
|
||||||
val tablesVersion = "0.2.0-dev-4"
|
val tablesVersion = "0.3.0"
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
@ -22,8 +22,8 @@ kscience {
|
|||||||
api("space.kscience:tables-kt:${tablesVersion}")
|
api("space.kscience:tables-kt:${tablesVersion}")
|
||||||
}
|
}
|
||||||
dependencies(jsMain) {
|
dependencies(jsMain) {
|
||||||
implementation(npm("tabulator-tables", "5.4.4"))
|
implementation(npm("tabulator-tables", "5.5.2"))
|
||||||
implementation(npm("@types/tabulator-tables", "5.4.8"))
|
implementation(npm("@types/tabulator-tables", "5.5.3"))
|
||||||
}
|
}
|
||||||
useSerialization()
|
useSerialization()
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import space.kscience.dataforge.meta.double
|
|||||||
import space.kscience.dataforge.meta.int
|
import space.kscience.dataforge.meta.int
|
||||||
import space.kscience.tables.ColumnHeader
|
import space.kscience.tables.ColumnHeader
|
||||||
import space.kscience.tables.ColumnTable
|
import space.kscience.tables.ColumnTable
|
||||||
|
import space.kscience.tables.fill
|
||||||
import space.kscience.tables.get
|
import space.kscience.tables.get
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
@ -18,7 +19,7 @@ internal class VisionOfTableTest {
|
|||||||
val y by ColumnHeader.typed<Value>()
|
val y by ColumnHeader.typed<Value>()
|
||||||
|
|
||||||
val table = ColumnTable<Value>(100) {
|
val table = ColumnTable<Value>(100) {
|
||||||
x.fill { it.asValue() }
|
fill(x, null) { it.asValue() }
|
||||||
y.values = x.values.map { it?.double?.pow(2)?.asValue() }
|
y.values = x.values.map { it?.double?.pow(2)?.asValue() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package space.kscience.visionforge.solid.three
|
package space.kscience.visionforge.solid.three
|
||||||
|
|
||||||
import space.kscience.dataforge.misc.Type
|
import space.kscience.dataforge.misc.DfType
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.startsWith
|
import space.kscience.dataforge.names.startsWith
|
||||||
import space.kscience.visionforge.Vision
|
import space.kscience.visionforge.Vision
|
||||||
@ -17,7 +17,7 @@ import kotlin.reflect.KClass
|
|||||||
/**
|
/**
|
||||||
* Builder and updater for three.js object
|
* Builder and updater for three.js object
|
||||||
*/
|
*/
|
||||||
@Type(TYPE)
|
@DfType(TYPE)
|
||||||
public interface ThreeFactory<in T : Vision> {
|
public interface ThreeFactory<in T : Vision> {
|
||||||
|
|
||||||
public val type: KClass<in T>
|
public val type: KClass<in T>
|
||||||
|
@ -24,7 +24,7 @@ public object ThreeLineFactory : ThreeFactory<PolyLine> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val material = ThreeMaterials.getLineMaterial(
|
val material = ThreeMaterials.getLineMaterial(
|
||||||
vision.properties.getMeta(SolidMaterial.MATERIAL_KEY),
|
vision.properties[SolidMaterial.MATERIAL_KEY],
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ public object ThreeMaterials {
|
|||||||
private val visionMaterialCache = HashMap<Vision, Material>()
|
private val visionMaterialCache = HashMap<Vision, Material>()
|
||||||
|
|
||||||
internal fun cacheMaterial(vision: Vision): Material = visionMaterialCache.getOrPut(vision) {
|
internal fun cacheMaterial(vision: Vision): Material = visionMaterialCache.getOrPut(vision) {
|
||||||
buildMaterial(vision.properties.getMeta(SolidMaterial.MATERIAL_KEY)).apply {
|
buildMaterial(vision.properties[SolidMaterial.MATERIAL_KEY]).apply {
|
||||||
cached = true
|
cached = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,11 +133,11 @@ public fun Mesh.setMaterial(vision: Vision) {
|
|||||||
} else {
|
} else {
|
||||||
material = vision.parent?.let { parent ->
|
material = vision.parent?.let { parent ->
|
||||||
//TODO cache parent material
|
//TODO cache parent material
|
||||||
ThreeMaterials.buildMaterial(parent.properties.getMeta(SolidMaterial.MATERIAL_KEY))
|
ThreeMaterials.buildMaterial(parent.properties[SolidMaterial.MATERIAL_KEY])
|
||||||
} ?: ThreeMaterials.cacheMaterial(vision)
|
} ?: ThreeMaterials.cacheMaterial(vision)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
material = ThreeMaterials.buildMaterial(vision.properties.getMeta(SolidMaterial.MATERIAL_KEY))
|
material = ThreeMaterials.buildMaterial(vision.properties[SolidMaterial.MATERIAL_KEY])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,18 +153,18 @@ public fun Mesh.updateMaterialProperty(vision: Vision, propertyName: Name) {
|
|||||||
when (propertyName) {
|
when (propertyName) {
|
||||||
SolidMaterial.MATERIAL_COLOR_KEY -> {
|
SolidMaterial.MATERIAL_COLOR_KEY -> {
|
||||||
material.asDynamic().color =
|
material.asDynamic().color =
|
||||||
vision.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).threeColor()
|
vision.properties[SolidMaterial.MATERIAL_COLOR_KEY].threeColor()
|
||||||
?: ThreeMaterials.DEFAULT_COLOR
|
?: ThreeMaterials.DEFAULT_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
SolidMaterial.SPECULAR_COLOR_KEY -> {
|
SolidMaterial.SPECULAR_COLOR_KEY -> {
|
||||||
material.asDynamic().specular =
|
material.asDynamic().specular =
|
||||||
vision.properties.getMeta(SolidMaterial.SPECULAR_COLOR_KEY).threeColor()
|
vision.properties[SolidMaterial.SPECULAR_COLOR_KEY].threeColor()
|
||||||
?: ThreeMaterials.DEFAULT_COLOR
|
?: ThreeMaterials.DEFAULT_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY -> {
|
SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY -> {
|
||||||
material.asDynamic().emissive = vision.properties.getMeta(SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY)
|
material.asDynamic().emissive = vision.properties[SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY]
|
||||||
.threeColor()
|
.threeColor()
|
||||||
?: ThreeMaterials.BLACK_COLOR
|
?: ThreeMaterials.BLACK_COLOR
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ public fun Mesh.applyEdges(vision: Solid) {
|
|||||||
val edges = children.find { it.name == EDGES_OBJECT_NAME } as? LineSegments
|
val edges = children.find { it.name == EDGES_OBJECT_NAME } as? LineSegments
|
||||||
//inherited edges definition, enabled by default
|
//inherited edges definition, enabled by default
|
||||||
if (vision.properties.getValue(EDGES_ENABLED_KEY, inherit = false)?.boolean != false) {
|
if (vision.properties.getValue(EDGES_ENABLED_KEY, inherit = false)?.boolean != false) {
|
||||||
val material = ThreeMaterials.getLineMaterial(vision.properties.getMeta(EDGES_MATERIAL_KEY), true)
|
val material = ThreeMaterials.getLineMaterial(vision.properties[EDGES_MATERIAL_KEY], true)
|
||||||
if (edges == null) {
|
if (edges == null) {
|
||||||
add(
|
add(
|
||||||
LineSegments(
|
LineSegments(
|
||||||
|
Loading…
Reference in New Issue
Block a user