Compare commits
No commits in common. "ed71ba9ccbf42e03779f480b2407c99ab8712046" and "64c447a37b9b63dd83ba2e6a8c3efcf246d96120" have entirely different histories.
ed71ba9ccb
...
64c447a37b
@ -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 non-nullable `invoke` instead of `set`.
|
- Color accessor property is now `colorProperty`. Color uses `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.
|
||||||
|
@ -12,7 +12,7 @@ val fxVersion by extra("11")
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.3.0-dev-16"
|
version = "0.3.0-dev-14"
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
@ -13,6 +13,7 @@ kscience {
|
|||||||
useKtor()
|
useKtor()
|
||||||
fullStack(
|
fullStack(
|
||||||
"muon-monitor.js",
|
"muon-monitor.js",
|
||||||
|
development = true,
|
||||||
jvmConfig = { withJava() },
|
jvmConfig = { withJava() },
|
||||||
jsConfig = { useCommonJs() }
|
jsConfig = { useCommonJs() }
|
||||||
) {
|
) {
|
||||||
@ -46,6 +47,9 @@ 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 {
|
||||||
|
@ -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
|
toolsVersion=0.15.0-kotlin-1.9.20-RC2
|
||||||
#kotlin.experimental.tryK2=true
|
#kotlin.experimental.tryK2=true
|
||||||
#kscience.wasm.disabled=true
|
#kscience.wasm.disabled=true
|
||||||
|
@ -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",
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
plugins {
|
|
||||||
id("space.kscience.gradle.mpp")
|
|
||||||
id("org.jetbrains.compose") version "1.5.10"
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,189 +0,0 @@
|
|||||||
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.ValueRequirement
|
|
||||||
import space.kscience.dataforge.meta.descriptors.get
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.remove
|
|
||||||
import space.kscience.dataforge.names.Name
|
|
||||||
import space.kscience.dataforge.names.NameToken
|
|
||||||
import space.kscience.dataforge.names.isEmpty
|
|
||||||
import space.kscience.dataforge.names.lastOrNull
|
|
||||||
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 rootDescriptor Full path to the displayed node in [meta]. Could be empty
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun PropertyEditorItem(
|
|
||||||
/**
|
|
||||||
* Root config object - always non-null
|
|
||||||
*/
|
|
||||||
meta: MutableMeta,
|
|
||||||
getPropertyState: (Name) -> EditorPropertyState,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
updates: Flow<Name>,
|
|
||||||
name: Name,
|
|
||||||
rootDescriptor: MetaDescriptor?,
|
|
||||||
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?.valueRequirement != ValueRequirement.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)
|
|
||||||
}) {
|
|
||||||
PropertyEditorItem(meta, getPropertyState, scope, updates, name, descriptor, expanded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
public fun PropertyEditor(
|
|
||||||
scope: CoroutineScope,
|
|
||||||
properties: ObservableMutableMeta,
|
|
||||||
descriptor: MetaDescriptor? = null,
|
|
||||||
expanded: Boolean? = null,
|
|
||||||
) {
|
|
||||||
PropertyEditorItem(
|
|
||||||
meta = properties,
|
|
||||||
getPropertyState = { name ->
|
|
||||||
if (properties[name] != null) {
|
|
||||||
EditorPropertyState.Defined
|
|
||||||
} else if (descriptor?.get(name)?.defaultValue != null) {
|
|
||||||
EditorPropertyState.Default("descriptor")
|
|
||||||
} else {
|
|
||||||
EditorPropertyState.Undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scope = scope,
|
|
||||||
updates = callbackFlow {
|
|
||||||
properties.onChange(scope) { name ->
|
|
||||||
scope.launch {
|
|
||||||
send(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeOnClose {
|
|
||||||
properties.removeListener(scope)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name = Name.EMPTY,
|
|
||||||
rootDescriptor = descriptor,
|
|
||||||
initialExpanded = expanded,
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
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,11 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun ThreeJs(){
|
||||||
|
Surface {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,94 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package space.kscience.visionforge.compose
|
|
||||||
|
|
||||||
import org.jetbrains.compose.web.css.*
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
@ -1,268 +0,0 @@
|
|||||||
@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.ValueRequirement
|
|
||||||
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?.valueRequirement != ValueRequirement.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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ 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.meta.Meta
|
|
||||||
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
|
||||||
@ -37,7 +36,7 @@ public interface Vision : Described {
|
|||||||
/**
|
/**
|
||||||
* Update this vision using a dif represented by [VisionChange].
|
* Update this vision using a dif represented by [VisionChange].
|
||||||
*/
|
*/
|
||||||
public fun receiveChange(change: VisionChange) {
|
public fun update(change: VisionChange) {
|
||||||
if (change.children?.isNotEmpty() == true) {
|
if (change.children?.isNotEmpty() == true) {
|
||||||
error("Vision is not a group")
|
error("Vision is not a group")
|
||||||
}
|
}
|
||||||
@ -46,20 +45,6 @@ public interface Vision : Described {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun onMetaEvent(meta: Meta){
|
|
||||||
//Do nothing by default
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receive and process a generic [VisionEvent].
|
|
||||||
*/
|
|
||||||
public fun receiveEvent(event: VisionEvent) {
|
|
||||||
when (event) {
|
|
||||||
is VisionChange -> receiveChange(event)
|
|
||||||
is VisionMetaEvent -> onMetaEvent(event.meta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val descriptor: MetaDescriptor?
|
override val descriptor: MetaDescriptor?
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
|
@ -63,7 +63,8 @@ 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,7 +13,7 @@ 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(targetName: Name, event: VisionEvent)
|
public suspend fun sendEvent(event: VisionEvent)
|
||||||
|
|
||||||
public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?)
|
public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?)
|
||||||
}
|
}
|
||||||
@ -35,8 +35,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(targetName: Name, payload: MetaRepr): Unit {
|
public fun VisionClient.sendEvent(visionName: Name, event: MetaRepr): Unit {
|
||||||
context.launch {
|
context.launch {
|
||||||
sendEvent(targetName, VisionMetaEvent(payload.toMeta()))
|
sendEvent(VisionMetaEvent(visionName, event.toMeta()))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,8 +11,10 @@ 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 companion object {
|
public val targetName: Name
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,14 +24,17 @@ public sealed interface VisionEvent {
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("meta")
|
@SerialName("meta")
|
||||||
public class VisionMetaEvent(public val meta: Meta) : VisionEvent
|
public class VisionMetaEvent(override val targetName: Name, 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")
|
public val Vision.Companion.CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the payload to be sent to server on click
|
* Set the payload to be sent to server on click
|
||||||
*/
|
*/
|
||||||
public fun Vision.onClickPayload(payloadBuilder: MutableMeta.() -> Unit) {
|
public fun Vision.onClickPayload(payloadBuilder: MutableMeta.() -> Unit){
|
||||||
properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder)
|
properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder)
|
||||||
}
|
}
|
@ -17,12 +17,12 @@ import space.kscience.visionforge.Vision.Companion.STYLE_KEY
|
|||||||
public interface VisionGroup : Vision {
|
public interface VisionGroup : Vision {
|
||||||
public val children: VisionChildren
|
public val children: VisionChildren
|
||||||
|
|
||||||
override fun receiveChange(change: VisionChange) {
|
override fun update(change: VisionChange) {
|
||||||
change.children?.forEach { (name, change) ->
|
change.children?.forEach { (name, change) ->
|
||||||
if (change.vision != null || change.vision == NullVision) {
|
if (change.vision != null || change.vision == NullVision) {
|
||||||
error("VisionGroup is read-only")
|
error("VisionGroup is read-only")
|
||||||
} else {
|
} else {
|
||||||
children.getChild(name)?.receiveChange(change)
|
children.getChild(name)?.update(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
change.properties?.let {
|
change.properties?.let {
|
||||||
@ -37,12 +37,12 @@ public interface MutableVisionGroup : VisionGroup {
|
|||||||
|
|
||||||
public fun createGroup(): MutableVisionGroup
|
public fun createGroup(): MutableVisionGroup
|
||||||
|
|
||||||
override fun receiveChange(change: VisionChange) {
|
override fun update(change: VisionChange) {
|
||||||
change.children?.forEach { (name, change) ->
|
change.children?.forEach { (name, change) ->
|
||||||
when {
|
when {
|
||||||
change.vision == NullVision -> children.setChild(name, null)
|
change.vision == NullVision -> children.setChild(name, null)
|
||||||
change.vision != null -> children.setChild(name, change.vision)
|
change.vision != null -> children.setChild(name, change.vision)
|
||||||
else -> children.getChild(name)?.receiveChange(change)
|
else -> children.getChild(name)?.update(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
change.properties?.let {
|
change.properties?.let {
|
||||||
|
@ -81,14 +81,15 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val eventCollector by lazy {
|
private val eventCollector by lazy {
|
||||||
MutableSharedFlow<Pair<Name, VisionEvent>>(meta["feedback.eventCache"].int ?: 100)
|
MutableSharedFlow<VisionEvent>(meta["feedback.eventCache"].int ?: 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a custom feedback event
|
* Send a custom feedback event
|
||||||
*/
|
*/
|
||||||
override suspend fun sendEvent(targetName: Name, event: VisionEvent) {
|
override suspend fun sendEvent(event: VisionEvent) {
|
||||||
eventCollector.emit(targetName to event)
|
eventCollector.emit(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderVision(element: Element, name: Name, vision: Vision, outputMeta: Meta) {
|
private fun renderVision(element: Element, name: Name, vision: Vision, outputMeta: Meta) {
|
||||||
@ -119,21 +120,19 @@ 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 event: VisionEvent = visionManager.jsonFormat.decodeFromString(
|
val change: VisionChange = visionManager.jsonFormat.decodeFromString(
|
||||||
VisionEvent.serializer(),
|
VisionChange.serializer(),
|
||||||
stringData
|
stringData
|
||||||
)
|
)
|
||||||
|
|
||||||
// If change contains root vision replacement, do it
|
// If change contains root vision replacement, do it
|
||||||
if(event is VisionChange) {
|
change.vision?.let { vision ->
|
||||||
event.vision?.let { vision ->
|
renderVision(element, name, vision, outputMeta)
|
||||||
renderVision(element, name, vision, outputMeta)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug { "Got $event for output with name $name" }
|
logger.debug { "Got update $change for output with name $name" }
|
||||||
if (vision == null) error("Can't update vision because it is not loaded.")
|
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" }
|
||||||
}
|
}
|
||||||
@ -148,8 +147,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
|
|
||||||
onopen = {
|
onopen = {
|
||||||
feedbackJob = visionManager.context.launch {
|
feedbackJob = visionManager.context.launch {
|
||||||
eventCollector.filter { it.first == name }.onEach {
|
eventCollector.filter { it.targetName == name }.onEach {
|
||||||
send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it.second))
|
send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it))
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
@ -157,7 +156,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
val change = changeCollector[name] ?: continue
|
val change = changeCollector[name] ?: continue
|
||||||
if (!change.isEmpty()) {
|
if (!change.isEmpty()) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
eventCollector.emit(name to change.deepCopy(visionManager))
|
eventCollector.emit(VisionChangeEvent(name, change.deepCopy(visionManager)))
|
||||||
change.reset()
|
change.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ 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
|
||||||
@ -72,11 +71,14 @@ 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)
|
||||||
@ -100,17 +102,16 @@ 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 { event ->
|
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update ->
|
||||||
val json = configuration.visionManager.jsonFormat.encodeToString(
|
val json = configuration.visionManager.jsonFormat.encodeToString(
|
||||||
VisionEvent.serializer(),
|
VisionChange.serializer(),
|
||||||
event
|
update
|
||||||
)
|
)
|
||||||
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))
|
||||||
@ -146,8 +147,6 @@ 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,
|
||||||
@ -155,7 +154,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() = value?.let { if (it == Null) null else it.string }
|
get() = this?.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) {
|
||||||
value = webColor.asValue()
|
this?.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) {
|
||||||
value = Colors.rgbToString(rgb).asValue()
|
this?.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) {
|
||||||
value = Colors.rgbToString(r, g, b).asValue()
|
this?.value = Colors.rgbToString(r, g, b).asValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ColorAccessor.clear() {
|
public fun ColorAccessor?.clear() {
|
||||||
value = null
|
this?.value = null
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,7 +28,7 @@ internal class VisionUpdateTest {
|
|||||||
propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
||||||
propertyChanged("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
propertyChanged("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
||||||
}
|
}
|
||||||
targetVision.receiveChange(dif)
|
targetVision.update(dif)
|
||||||
assertTrue { targetVision.children.getChild("top") is SolidGroup }
|
assertTrue { targetVision.children.getChild("top") is SolidGroup }
|
||||||
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
|
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
}
|
}
|
||||||
|
|
||||||
val tablesVersion = "0.2.1"
|
val tablesVersion = "0.2.0-dev-4"
|
||||||
|
|
||||||
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.5.2"))
|
implementation(npm("tabulator-tables", "5.4.4"))
|
||||||
implementation(npm("@types/tabulator-tables", "5.5.3"))
|
implementation(npm("@types/tabulator-tables", "5.4.8"))
|
||||||
}
|
}
|
||||||
useSerialization()
|
useSerialization()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user