Merge branch 'dev' into teldufalsari/dev

This commit is contained in:
Igor Dunaev 2023-12-10 15:54:11 +03:00
commit c0cf852c62
69 changed files with 1910 additions and 334 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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)
} }
} }

View File

@ -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 {

View File

@ -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()
} }

View File

@ -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,

View 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
}

View File

@ -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,

View File

@ -75,7 +75,7 @@ fun main() {
server.openInBrowser() server.openInBrowser()
while (readln() != "exit") { while (readlnOrNull() != "exit") {
} }

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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) {

View 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)
}
}
}
}

View File

@ -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)
}

View File

@ -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())
}
}
}
}
}

View File

@ -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,
)
}

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -1,11 +0,0 @@
package space.kscience.visionforge.compose
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
@Composable
public fun ThreeJs(){
Surface {
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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) }

View File

@ -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)
}

View File

@ -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
)

View File

@ -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)
}
}

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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 {

View File

@ -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]

View File

@ -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()))
} }
} }

View File

@ -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)
}

View File

@ -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())
} }
} }

View File

@ -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 =

View File

@ -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))
} }
} }
} }

View File

@ -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)

View File

@ -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(

View File

@ -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()
}

View File

@ -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)
} }

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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 ->
event.preventDefault()
val formData = FormData(form).toMeta()
vision.values = formData
console.info("Sent: ${formData.toMap()}")
false
} }
} }
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData)
console.info("Sent: ${formData.toMap()}")
false
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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()

View File

@ -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,

View File

@ -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()

View File

@ -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
} }

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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)
} }

View File

@ -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) {

View File

@ -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
} }
} }

View File

@ -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
) )

View File

@ -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
} }

View File

@ -94,7 +94,7 @@ class SolidPropertyTest {
} }
} }
} }
assertEquals("#555555", box?.color.string) assertEquals("#555555", box?.color?.string)
} }
@Test @Test

View File

@ -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)
} }
} }

View File

@ -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()
} }

View File

@ -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() }
} }

View File

@ -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>

View File

@ -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
) )

View File

@ -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
} }

View File

@ -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(