Compare commits

..

12 Commits

64 changed files with 1560 additions and 309 deletions

View File

@ -7,12 +7,12 @@ plugins {
// 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")
allprojects {
group = "space.kscience"
version = "0.3.0-dev-16"
version = "0.3.0-dev-17"
}
subprojects {

View File

@ -34,6 +34,6 @@ class GDMLVisionTest {
val child = cubes[Name.of("composite-000","segment-0")]
assertNotNull(child)
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

@ -71,7 +71,7 @@ class Model(val manager: VisionManager) {
fun reset() {
map.values.forEach {
it.properties.setMeta(SolidMaterial.MATERIAL_COLOR_KEY, null)
it.properties[SolidMaterial.MATERIAL_COLOR_KEY] = null
}
tracks.children.clear()
}

View File

@ -54,9 +54,6 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
"jupyter": {
"outputs_hidden": false
},
"tags": []
},
"outputs": [],
@ -83,9 +80,6 @@
"language": "kotlin",
"name": "kotlin"
},
"ktnbPluginMetadata": {
"isAddProjectLibrariesToClasspath": false
},
"language_info": {
"codemirror_mode": "text/x-kotlin",
"file_extension": ".kt",
@ -94,6 +88,9 @@
"nbconvert_exporter": "",
"pygments_lexer": "kotlin",
"version": "1.8.20"
},
"ktnbPluginMetadata": {
"projectLibraries": []
}
},
"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",
"execution_count": null,
"metadata": {
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
"collapsed": false
},
"outputs": [],
"source": [
@ -84,7 +81,7 @@
"version": "1.8.0-dev-3517"
},
"ktnbPluginMetadata": {
"isAddProjectLibrariesToClasspath": false
"projectLibraries": []
}
},
"nbformat": 4,

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,416 @@
# Event Display Tutorial
In this tutorial, we will explore properties of Visions and build a simple front-end application. You may find a complete project [here](https://git.sciprog.center/teldufalsari/visionforge-event-display-demo).
__NOTE:__ You will need Kotlin Multiplatform 1.9.0 or higher to complete this tutorial!
### Starting the Project
We will use Idea's default project template for Kotlin Multiplatform. To initialize the project, go to *File -> New -> Project...*, Then choose *Full-Stack Web Application* project template and
*Kotlin Gradle* build system. Then select *Next -> Finish*. You will end up with a project with some sample code.
To check that everything is working correctly, run *application -> run* Gradle target. You should see a greeting page when you open `http://localhost:8080` in a web browser.
We will use Kotlin React as our main UI library and Ktor Netty both as a web server. Our event display frontend and server will reside in `jsMain` and `jvmMain` directories respectively.
Before we start, we have to load necessary dependencies:
* Add SciProgCentre maven repo in `build.gradle.kts` file:
```kotlin
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven")
// Add either the line below:
maven("https://repo.kotlin.link")
// Or this line:
maven("https://maven.sciprog.center/kscience")
}
```
* Add `visionforge-threejs-server` into the list of JS dependencies of your project:
```kotlin
kotlin {
sourceSets {
val jsMain by getting {
dependencies {
implementation("space.kscience:visionforge-threejs-server:0.3.0-dev-14")
}
}
}
}
```
Refresh build model in the Idea to make sure the dependencies are successfully resolved.
__NOTE:__ In previous versions of VisionForge, some imports may be broken. If these dependencies fail to resolve, replace `space.kscience:visionforge-threejs-server:0.3.0-dev-14` with `space.kscience:visionforge-threejs:0.3.0-dev-14`. The resulting bundle will lack a React component used in the tutorial (see "Managing Visions"). You may copy and paste it directly from either [VisionForge](https://git.sciprog.center/kscience/visionforge/src/branch/master/ui/react/src/main/kotlin/space/kscience/visionforge/react/ThreeCanvasComponent.kt) or the [tutorial repo](https://git.sciprog.center/teldufalsari/visionforge-event-display-demo/src/branch/main/src/jsMain/kotlin/canvas/ThreeCanvasComponent.kt), or even come up with a better implementation if your own.
### Setting up Page Markup
We need to create a page layout and set up Netty to serve our page to clients. There is nothing special related to VisionForge, so feel free to copy and paste the code below.
File: `src/jvmMain/.../Server.kt`
```kotlin
// ... imports go here
fun HTML.index() {
head {
// Compatibility headers
meta { charset = "UTF-8" }
meta {
name = "viewport"
content = "width=device-width, initial-scale=1.0"
}
meta {
httpEquiv = "X-UA-Compatible"
content = "IE=edge"
}
title("VF Demo")
}
// Link to our react script
body {
script(src = "/static/vf-demo.js") {}
}
}
fun main() {
// Seting up Netty
embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
routing {
get("/") {
call.respondHtml(HttpStatusCode.OK, HTML::index)
}
static("/static") {
resources()
}
}
}.start(wait = true)
}
```
File: `src/jsMain/.../Client.kt`
```kotlin
fun main() {
val container = document.createElement("div")
document.body!!.appendChild(container)
val eventDisplay = EventDisplay.create {}
createRoot(container).render(eventDisplay)
}
```
File: `src/jsMain/.../Display.kt`
```kotlin
// All markup goes here:
val EventDisplay = FC<Props> {
// Global CSS rules
Global {
styles {
"html,\n" +
"body" {
height = 100.vh
width = 100.vw
margin = 0.px
}
"body > div" {
height = 100.vh
width = 100.vw
display = Display.flex
flexDirection = FlexDirection.column
justifyContent = JustifyContent.start
alignItems = AlignItems.center
}
"*,\n" +
"*:before,\n" +
"*:after" {
boxSizing = BoxSizing.borderBox
}
}
}
div {
css {
height = 100.pct
width = 100.pct
display = Display.flex
flexDirection = FlexDirection.column
alignItems = AlignItems.center
}
div {
css {
width = 100.pct
display = Display.flex
flexDirection = FlexDirection.row
alignItems = AlignItems.center
justifyContent = JustifyContent.center
}
input {
css {
margin = 5.px
padding = 5.px
}
type = InputType.button
value = "Update Events"
}
input {
css {
margin = 5.px
padding = 5.px
}
type = InputType.button
value = "Update Geometry"
}
}
div {
css {
width = 98.pct
height = 1.pct
margin = 5.px
display = Display.flex
flexGrow = number(1.0)
justifyContent = JustifyContent.center
alignItems = AlignItems.center
backgroundColor = Color("#b3b3b3")
}
}
}
}
```
After setting everything up, you should see a gray rectangle with two buttons above it when opening `localhost:8080`.
### Managing Visions
We are approaching the main part of the tutorial - the place where we will create a working demo. In particle accelerator experiments, event displays are employed to visualise particle collision events. Essentially, it requires drawing a detector setup and visual interpretation of events: tracks, detector hits etc. Usually, a number of events share a common detector setup (e.g. if these events occured in a single experiment run). It makes sense to update and re-render only event information, while keeping detector geometry constant between updates.
Visions (namely, the `SolidGroup` class) allow us to create an object tree for our displayed event. `SolidGroup` can hold other Visions as its child nodes, access these nodes by names and update/delete them. We will use this property to update our event display efficiently.
To display Visions as actual 3D object, we will use `ThreePlugin` that renders Visions using *three.js* library. The plugin allows us to create a Three.js representation of a vision that will observe changes of its correspondent Vision. This way we can update only Visions without diving deep into three.js stuff. Using observable Visions is also efficient: Three.js representations are not generated from scratch after each Vision update but are modified too.
First, let's simulate data load operations:
* Add state variables to our `EventDisplay` React component. These variables will be treated as data loaded from a remote server. In real life, these may be JSON string with event data:
```kotlin
val EventDisplay = FC<Props> {
// ...
var eventData: kotlin.Float? by useState(null)
var geometryData: kotlin.Float? by useState(null)
// ...
}
```
* Write two simple functions that will convert data to a Vision. In this case, we will simply parameters of solids like color of size; in real life, these functions will usually take raw data and convert it into Visions.
```kotlin
fun generateEvents(radius: Float): SolidGroup {
val count = Random.nextInt(10, 20)
return SolidGroup {
repeat(count) {
sphere(radius) {
x = 5.0 * (Random.nextFloat() - 0.5)
y = 2.0 * (Random.nextFloat() - 0.5)
z = 2.0 * (Random.nextFloat() - 0.5)
color(Colors.red)
}
}
}
}
fun generateGeometry(distance: Float): SolidGroup {
return SolidGroup {
box(10, 3, 3) {
x = 0.0
y = -distance
z = 0.0
color(Colors.gray)
}
box(10, 3, 3) {
x = 0.0
y = distance
z = 0.0
color(Colors.gray)
}
}
}
```
* Then, let's create our main Vision and add a static light source:
```kotlin
val EventDisplay = FC<Props> {
// ...
val containedVision: SolidGroup by useState(SolidGroup {
ambientLight {
color(Colors.white)
}
})
// ...
}
```
* A `Context` object is required to hold plugins like `ThreePlugin`. It is also necessary to make Visions observable: we have to root our main Vision in the context. Declare a global `Context` in the same file with `EventDisplay` component:
```kotlin
val viewContext = Context {
plugin(Solids)
plugin(ThreePlugin)
}
```
* Import `ThreeCanvasComponent` from VisionForge. This is a React component that handles all display work. It creates three.js canvas, attaches it to its own parent element and creates and draws `Object3D` on the canvas. We will attach this component to a
separate React component. Note order for Visions to update their Three.js representations, these Visions need to be rooted in a `Context`. This way Visions will be observed for changes, and any such change will trigger an update of the corresponding Three.js object.
```kotlin
external interface EventViewProps: Props {
var displayedVision: Solid?
var context: Context
}
val EventView = FC<EventViewProps> { props ->
ThreeCanvasComponent {
solid = props.displayedVision
context = props.context
}
// Make displayedVision observed:
useEffect(props.displayedVision) {
props.displayedVision?.setAsRoot(props.context.visionManager)
}
}
```
__NOTE:__ If you had problems with dependency resolution, `ThreeCanvasComponent` may missing from your import scope. You may find a compatible implementation [here](https://git.sciprog.center/teldufalsari/visionforge-event-display-demo/src/branch/main/src/jsMain/kotlin/canvas/ThreeCanvasComponent.kt).
* Finally, we need to attach EventView to our main component and connect raw data updates to Vision updates using React hooks:
```kotlin
// ...
// Names used as keys to access and update Visions
// Refer to DataForge documentation for more details
val EVENTS_NAME = "DEMO_EVENTS".parseAsName(false)
val GEOMETRY_NAME = "DEMO_GEOMETRY".parseAsName(false)
// ...
val EventDisplay = FC<Props> {
// ...
useEffect(eventData) {
eventData?.let {
containedVision.setChild(EVENTS_NAME, generateEvents(it))
}
}
useEffect(geometryData) {
geometryData?.let {
containedVision.setChild(GEOMETRY_NAME, generateGeometry(it))
}
}
// ...
div {
// ...
div {
css {
width = 98.pct
height = 1.pct
flexGrow = number(1.0)
margin = 5.px
display = Display.flex
justifyContent = JustifyContent.center
alignItems = AlignItems.center
}
// Replace the gray rectangle with an EventView:
EventView {
displayedVision = containedVision
context = viewContext
}
}
}
// ...
}
```
When we press either of the buttons, corresponding raw data changes. This update triggers `UseEffect` hook, which generates new event or geometry data and replaces the old data in the main Vision. Three.js representation is then updated to match our new Vision, so that changes are visible on the canvas.
Recompile the project and go on `http://localhost:8080`. See how the displayed scene changes with each click: for example, when you update geometry, only the distance between "magnets" varies, but spheres remain intact.
### Clearing the Scene
We can erase children Visions from the scene completely. To do so, we cat pass `null` to the function `setChild` as `child` argument. Add these lines to the hooks that update Visions to remove the corresponding Vision from our diplayed `SolidGroup` when raw data changes to `null`:
```kotlin
useEffect(eventData) {
// ...
if (eventData == null) {
containedVision.setChild(EVENT_NAME, null)
}
}
useEffect(geometryData) {
// ...
if (geometryData == null) {
containedVision.setChild(GEOMETRY_NAME, null)
}
}
```
To test how this works, let's create an erase button that will completely clear the scene:
```kotlin
val EventDisplay = FC<Props> {
// ...
div {
// ...
input {
css {
margin = 5.px
padding = 5.px
backgroundColor = NamedColor.lightcoral
color = NamedColor.white
}
type = InputType.button
value = "Clear Scene"
onClick = {
geometryData = null
eventData = null
}
}
}
// ...
}
```
![Picture of an event display with a red button with a caption "Clear Scene" added to the two previous buttons](../images/event-display-final.png "Scene clearing function")
### Making Selection Fine-Grained
You may feel annoyed by how selection works in our demo. That's right, selecting the whole detector or the entire event array is not that useful. This is due to the fact that VisionForge selects object based on names. We used names to distinguish SolidGroups, but in fact not only groups but every single Vision can have a name. But when we were randomly generating Vision, we did not use any names, did we? Right, Vision can be nameless, in which case they are treated as a monolithic object together with their parent. So it should be clear now that when we were selecting a single rectangle, we were in fact selecting the whole pair of rectangles and the other one went lit up as well.
Fortunately, every `Solid` constructor takes a `name` parameter after essential parameters, so it should be easy to fix it. Go to the generator functions and add the change construction invocations to the following:
```kotlin
fun generateGeometry(distance: Float): SolidGroup {
return SolidGroup {
box(10, 3, 3, "Magnet1") {
// ...
}
box(10, 3, 3, "Magnet2") {
// ...
}
}
}
```
For the events part, we will use the index in a loop as a name:
```kotlin
fun generateEvents(radius: Float): SolidGroup {
// ...
repeat(count) {
sphere(radius, it.toString()) {
// ...
}
```
After you update the build, it should be possible to select only one sphere or rectangle:
![Picture of an event display with only one sphere between the magnets selected](../images/event-display-selection.png "Selection demonstration")

View File

@ -8,6 +8,6 @@ org.gradle.jvmargs=-Xmx4G
org.jetbrains.compose.experimental.jscanvas.enabled=true
toolsVersion=0.15.0-kotlin-1.9.20
toolsVersion=0.15.2-kotlin-1.9.21
#kotlin.experimental.tryK2=true
#kscience.wasm.disabled=true

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists

View File

@ -29,7 +29,7 @@ public fun RBuilder.visionPropertyEditor(
this.descriptor = descriptor
this.scope = vision.manager?.context ?: error("Orphan vision could not be observed")
this.getPropertyState = { name ->
val ownMeta = vision.properties.own?.getMeta(name)
val ownMeta = vision.properties.own?.get(name)
if (ownMeta != null && !ownMeta.isEmpty()) {
EditorPropertyState.Defined
} else if (vision.properties.root().getValue(name) != null) {

View File

@ -1,7 +1,8 @@
plugins {
id("space.kscience.gradle.mpp")
id("org.jetbrains.compose") version "1.5.10"
alias(spclibs.plugins.compose)
// id("org.jetbrains.compose") version "1.5.11"
// id("com.android.library")
}

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

@ -17,14 +17,10 @@ import org.jetbrains.compose.web.dom.Text
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.ObservableMutableMeta
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.ValueRequirement
import space.kscience.dataforge.meta.descriptors.ValueRestriction
import space.kscience.dataforge.meta.descriptors.get
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.remove
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.lastOrNull
import space.kscience.dataforge.names.*
import space.kscience.visionforge.hidden
@ -39,19 +35,17 @@ public sealed class EditorPropertyState {
}
/**
* @param meta Root config object - always non-null
* @param rootDescriptor Full path to the displayed node in [meta]. Could be empty
*/
@Composable
private fun PropertyEditorItem(
/**
* Root config object - always non-null
*/
public fun PropertyEditor(
scope: CoroutineScope,
meta: MutableMeta,
getPropertyState: (Name) -> EditorPropertyState,
scope: CoroutineScope,
updates: Flow<Name>,
name: Name,
rootDescriptor: MetaDescriptor?,
name: Name = Name.EMPTY,
rootDescriptor: MetaDescriptor? = null,
initialExpanded: Boolean? = null,
) {
var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) }
@ -109,7 +103,7 @@ private fun PropertyEditorItem(
Text(token)
}
if (!name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) {
if (!name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) {
Div({
style {
width(160.px)
@ -145,7 +139,7 @@ private fun PropertyEditorItem(
Div({
classes(TreeStyles.treeItem)
}) {
PropertyEditorItem(meta, getPropertyState, scope, updates, name, descriptor, expanded)
PropertyEditor(scope, meta, getPropertyState, updates, name + token, descriptor, expanded)
}
}
}
@ -159,7 +153,8 @@ public fun PropertyEditor(
descriptor: MetaDescriptor? = null,
expanded: Boolean? = null,
) {
PropertyEditorItem(
PropertyEditor(
scope = scope,
meta = properties,
getPropertyState = { name ->
if (properties[name] != null) {
@ -170,7 +165,6 @@ public fun PropertyEditor(
EditorPropertyState.Undefined
}
},
scope = scope,
updates = callbackFlow {
properties.onChange(scope) { name ->
scope.launch {

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

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

@ -1,6 +1,7 @@
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,
@ -33,3 +34,11 @@ public fun StyleScope.marginAll(
) {
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

@ -16,7 +16,7 @@ import org.w3c.dom.HTMLOptionElement
import org.w3c.dom.asList
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.ValueRequirement
import space.kscience.dataforge.meta.descriptors.ValueRestriction
import space.kscience.dataforge.meta.descriptors.allowedValues
import space.kscience.visionforge.Colors
import space.kscience.visionforge.widgetType
@ -199,7 +199,7 @@ public fun RangeValueChooser(
FlexRow {
if (descriptor?.valueRequirement != ValueRequirement.REQUIRED) {
if (descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
Input(type = InputType.Checkbox) {
if (!rangeDisabled) defaultChecked()

View File

@ -16,9 +16,8 @@ import react.dom.attrs
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.ObservableMutableMeta
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.ValueRequirement
import space.kscience.dataforge.meta.descriptors.ValueRestriction
import space.kscience.dataforge.meta.descriptors.get
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.remove
import space.kscience.dataforge.names.*
import space.kscience.visionforge.hidden
@ -146,7 +145,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
}
+token
}
if (!props.name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) {
if (!props.name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) {
styledDiv {
css {
//+TreeStyles.resizeableInput
@ -185,7 +184,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
}
+"\u00D7"
attrs {
if (editorPropertyState!= EditorPropertyState.Defined) {
if (editorPropertyState != EditorPropertyState.Defined) {
disabled = true
} else {
onClickFunction = removeClick

View File

@ -12,7 +12,7 @@ import react.dom.attrs
import react.fc
import react.useState
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.get
import space.kscience.dataforge.meta.string
@ -43,7 +43,7 @@ public val RangeValueChooser: FC<ValueChooserProps> = fc("RangeValueChooser") {
}
flexRow {
if (props.descriptor?.valueRequirement != ValueRequirement.REQUIRED) {
if (props.descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
styledInput(type = InputType.checkBox) {
attrs {
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
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
@ -23,7 +23,7 @@ public value class StyleSheet(private val owner: Vision) {
* Define a style without notifying owner
*/
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].
*/
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

View File

@ -4,12 +4,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.context.warn
import space.kscience.dataforge.meta.asValue
import space.kscience.dataforge.meta.boolean
import space.kscience.dataforge.meta.descriptors.Described
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.asName
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
@ -18,7 +19,7 @@ import space.kscience.visionforge.Vision.Companion.TYPE
/**
* A root type for display hierarchy
*/
@Type(TYPE)
@DfType(TYPE)
public interface Vision : Described {
/**
@ -37,7 +38,7 @@ public interface Vision : Described {
/**
* Update this vision using a dif represented by [VisionChange].
*/
public fun receiveChange(change: VisionChange) {
public fun update(change: VisionChange) {
if (change.children?.isNotEmpty() == true) {
error("Vision is not a group")
}
@ -46,18 +47,12 @@ public interface Vision : Described {
}
}
public fun onMetaEvent(meta: Meta){
//Do nothing by default
}
/**
* Receive and process a generic [VisionEvent].
*/
public fun receiveEvent(event: VisionEvent) {
when (event) {
is VisionChange -> receiveChange(event)
is VisionMetaEvent -> onMetaEvent(event.meta)
}
if(event is VisionChange) update(event)
else manager?.logger?.warn { "Undispatched event: $event" }
}
override val descriptor: MetaDescriptor?

View File

@ -18,7 +18,6 @@ public interface VisionClient: Plugin {
public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?)
}
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) {
notifyPropertyChanged(visionName, propertyName.parseAsName(true), item)
}

View File

@ -10,10 +10,17 @@ import space.kscience.visionforge.VisionChildren.Companion.STATIC_TOKEN_BODY
@DslMarker
public annotation class VisionBuilder
/**
* A container interface with read access to its content
* using DataForge [Name] objects as keys.
*/
public interface VisionContainer<out V : Vision> {
public fun getChild(name: Name): V?
}
/**
* A container interface with write/replace/delete access to its content.
*/
public interface MutableVisionContainer<in V : Vision> {
//TODO add documentation
public fun setChild(name: Name?, child: V?)
@ -61,12 +68,22 @@ public inline fun VisionChildren.forEach(block: (NameToken, Vision) -> Unit) {
keys.forEach { block(it, get(it)!!) }
}
/**
* A serializable representation of [Vision] children container
* with the ability to modify the container content.
*/
public interface MutableVisionChildren : VisionChildren, MutableVisionContainer<Vision> {
public override val parent: MutableVisionGroup
public operator fun set(token: NameToken, value: Vision?)
/**
* Set child [Vision] by name.
* @param name child name. Pass null to add a static child. Note that static children cannot
* be removed, replaced or accessed by name by other means.
* @param child new child value. Pass null to delete the child.
*/
override fun setChild(name: Name?, child: Vision?) {
when {
name == null -> {

View File

@ -3,8 +3,6 @@ package space.kscience.visionforge
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.set
import space.kscience.dataforge.names.Name
/**
@ -23,13 +21,3 @@ public sealed interface VisionEvent {
@Serializable
@SerialName("meta")
public class VisionMetaEvent(public val meta: Meta) : 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

@ -17,12 +17,12 @@ import space.kscience.visionforge.Vision.Companion.STYLE_KEY
public interface VisionGroup : Vision {
public val children: VisionChildren
override fun receiveChange(change: VisionChange) {
override fun update(change: VisionChange) {
change.children?.forEach { (name, change) ->
if (change.vision != null || change.vision == NullVision) {
error("VisionGroup is read-only")
} else {
children.getChild(name)?.receiveChange(change)
children.getChild(name)?.update(change)
}
}
change.properties?.let {
@ -37,12 +37,12 @@ public interface MutableVisionGroup : VisionGroup {
public fun createGroup(): MutableVisionGroup
override fun receiveChange(change: VisionChange) {
override fun update(change: VisionChange) {
change.children?.forEach { (name, change) ->
when {
change.vision == NullVision -> children.setChild(name, null)
change.vision != null -> children.setChild(name, change.vision)
else -> children.getChild(name)?.receiveChange(change)
else -> children.getChild(name)?.update(change)
}
}
change.properties?.let {

View File

@ -13,10 +13,7 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.toJson
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.VisionOfCheckbox
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionOfNumberField
import space.kscience.visionforge.html.VisionOfTextField
import space.kscience.visionforge.html.*
public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionContainer<Vision> {
override val tag: PluginTag get() = Companion.tag
@ -72,9 +69,11 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont
defaultDeserializer { SimpleVisionGroup.serializer() }
subclass(NullVision.serializer())
subclass(SimpleVisionGroup.serializer())
subclass(VisionOfHtmlInput.serializer())
subclass(VisionOfNumberField.serializer())
subclass(VisionOfTextField.serializer())
subclass(VisionOfCheckbox.serializer())
subclass(VisionOfRangeField.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 includeStyles toggles inclusion of properties from styles.
*/
public fun getMeta(
public fun get(
name: Name,
inherit: Boolean?,
includeStyles: Boolean? = null,
): 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>
@ -54,7 +54,7 @@ public interface VisionProperties : MetaProvider {
public interface MutableVisionProperties : VisionProperties, MutableMetaProvider {
override fun getMeta(
override fun get(
name: Name,
inherit: Boolean?,
includeStyles: Boolean?,
@ -65,7 +65,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
includeStyles,
)
public fun setMeta(
public fun set(
name: Name,
node: Meta?,
notify: Boolean,
@ -77,10 +77,10 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
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?) {
setMeta(name, node, true)
override fun set(name: Name, node: Meta?) {
set(name, node, true)
}
override fun setValue(name: Name, value: Value?) {
@ -89,7 +89,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
}
public fun MutableVisionProperties.remove(name: Name) {
setMeta(name, null)
set(name, null)
}
public fun MutableVisionProperties.remove(name: String) {
@ -114,7 +114,7 @@ private class VisionPropertiesItem(
override val items: Map<NameToken, MutableMeta>
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 defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet()
val inheritFlag = descriptor?.inherited ?: inherit
@ -148,8 +148,8 @@ private class VisionPropertiesItem(
default
)
override fun setMeta(name: Name, node: Meta?) {
properties.setMeta(nodeName + name, node)
override fun set(name: Name, node: Meta?) {
properties[nodeName + name] = node
}
override fun toString(): String = Meta.toString(this)
@ -202,16 +202,16 @@ public abstract class AbstractVisionProperties(
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
if (own?.getMeta(name) == node) return
if (own?.get(name) == node) return
if (name.isEmpty()) {
properties = node?.asMutableMeta()
} else if (node == null) {
properties?.setMeta(name, node)
properties?.set(name, node)
} else {
getOrCreateProperties().setMeta(name, node)
getOrCreateProperties()[name] = node
}
if (notify) {
invalidate(name)
@ -223,7 +223,7 @@ public abstract class AbstractVisionProperties(
if (own?.getValue(name) == value) return
if (value == null) {
properties?.getMeta(name)?.value = null
properties?.get(name)?.value = null
} else {
getOrCreateProperties().setValue(name, value)
}
@ -272,11 +272,11 @@ public fun VisionProperties.getValue(
/**
* Get [Vision] property using key as a String
*/
public fun VisionProperties.getMeta(
public fun VisionProperties.get(
name: String,
inherit: 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
@ -286,17 +286,17 @@ public fun VisionProperties.getMeta(
public fun MutableVisionProperties.root(
inherit: 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
*/
public fun MutableVisionProperties.getMeta(
public fun MutableVisionProperties.get(
name: String,
inherit: 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 =

View File

@ -17,10 +17,10 @@ public fun Vision.flowProperty(
includeStyles: Boolean? = null,
): Flow<Meta> = flow {
//Pass initial value.
emit(properties.getMeta(propertyName, inherit, includeStyles))
emit(properties.get(propertyName, inherit, includeStyles))
properties.changes.collect { name ->
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.node
/**
* @param formId an id of the element in rendered DOM, this form is bound to
*/
@Serializable
@SerialName("html.form")
public class VisionOfHtmlForm(
public val formId: String,
) : VisionOfHtmlInput() {
public var values: Meta? by mutableProperties.node()
) : VisionOfHtml() {
public var values: Meta? by properties.node()
}
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,
): Job {
//Pass initial value.
callback(properties.getMeta(propertyName, inherit, includeStyles))
callback(properties.get(propertyName, inherit, includeStyles))
return properties.changes.onEach { name ->
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"))
}
@ -39,9 +39,22 @@ public fun Vision.useProperty(
callback: (Meta) -> Unit,
): 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(
property: KProperty1<V, T>,
scope: CoroutineScope? = manager?.context,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
callback: V.(T) -> Unit,
): Job {
//Pass initial value.
@ -50,5 +63,5 @@ public fun <V : Vision, T> V.useProperty(
if (name.startsWith(property.name.asName())) {
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
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
@ -24,7 +23,6 @@ private class TestScheme : Scheme() {
companion object : SchemeSpec<TestScheme>(::TestScheme)
}
@OptIn(ExperimentalCoroutinesApi::class)
internal class VisionPropertyTest {
private val manager = Global.request(VisionManager)
@ -42,7 +40,7 @@ internal class VisionPropertyTest {
@Test
fun testPropertyEdit() {
val vision = manager.group()
vision.properties.getMeta("fff.ddd").apply {
vision.properties.get("fff.ddd").apply {
value = 2.asValue()
}
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
@ -52,7 +50,7 @@ internal class VisionPropertyTest {
@Test
fun testPropertyUpdate() {
val vision = manager.group()
vision.properties.getMeta("fff").updateWith(TestScheme) {
vision.properties.get("fff").updateWith(TestScheme) {
ddd = 2
}
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
@ -87,7 +85,7 @@ internal class VisionPropertyTest {
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(2, callCounter)
subscription.cancel()

View File

@ -9,8 +9,8 @@ import kotlinx.serialization.serializerOrNull
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.misc.Named
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
@ -20,13 +20,13 @@ import kotlin.reflect.cast
/**
* A browser renderer for a [Vision].
*/
@Type(ElementVisionRenderer.TYPE)
@DfType(ElementVisionRenderer.TYPE)
public interface ElementVisionRenderer : Named {
/**
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
* can't process a vision. The value of [DEFAULT_RATING] used for default renderer. Specialized renderers could specify
* higher value in order to "steal" rendering job
* higher value to "steal" rendering job
*/
public fun rateVision(vision: Vision): Int

View File

@ -67,7 +67,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
private val mutex = Mutex()
private val changeCollector = VisionChangeBuilder()
private val rootChangeCollector = VisionChangeBuilder()
/**
* Communicate vision property changed from rendering engine to model
@ -75,7 +76,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
override fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) {
context.launch {
mutex.withLock {
changeCollector.propertyChanged(visionName, propertyName, item)
rootChangeCollector.propertyChanged(visionName, propertyName, item)
}
}
}
@ -97,7 +98,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
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 ->
val wsUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) {
val endpoint = resolveEndpoint(element)
@ -109,9 +110,10 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
URL(attr.value)
}.apply {
protocol = "ws"
searchParams.append("name", name.toString())
searchParams.append("name", visionName.toString())
}
logger.info { "Updating vision data from $wsUrl" }
//Individual websocket for this vision
@ -125,21 +127,19 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
)
// If change contains root vision replacement, do it
if(event is VisionChange) {
if (event is VisionChange) {
event.vision?.let { vision ->
renderVision(element, name, vision, outputMeta)
renderVision(element, visionName, vision, outputMeta)
}
}
logger.debug { "Got $event for output with name $name" }
if (vision == null) error("Can't update vision because it is not loaded.")
logger.debug { "Got $event for output with name $visionName" }
vision.receiveEvent(event)
} else {
logger.error { "WebSocket message data is not a string" }
}
}
//Backward change propagation
var feedbackJob: Job? = null
@ -147,32 +147,35 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
val feedbackAggregationTime = meta["feedback.aggregationTime"]?.int ?: 300
onopen = {
feedbackJob = visionManager.context.launch {
eventCollector.filter { it.first == name }.onEach {
//launch a separate coroutine to send events to the backend
eventCollector.filter { it.first == visionName }.onEach {
send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it.second))
}.launchIn(this)
//aggregate atomic changes
while (isActive) {
delay(feedbackAggregationTime.milliseconds)
val change = changeCollector[name] ?: continue
if (!change.isEmpty()) {
val visionChangeCollector = rootChangeCollector[name]
if (visionChangeCollector?.isEmpty() == false) {
mutex.withLock {
eventCollector.emit(name to change.deepCopy(visionManager))
change.reset()
eventCollector.emit(visionName to visionChangeCollector.deepCopy(visionManager))
rootChangeCollector.reset()
}
}
}
}
logger.info { "WebSocket feedback channel established for output '$name'" }
logger.info { "WebSocket feedback channel established for output '$visionName'" }
}
onclose = {
feedbackJob?.cancel()
logger.info { "WebSocket feedback channel closed for output '$name'" }
logger.info { "WebSocket feedback channel closed for output '$visionName'" }
}
onerror = {
feedbackJob?.cancel()
logger.error { "WebSocket feedback channel error for output '$name'" }
logger.error { "WebSocket feedback channel error for output '$visionName'" }
}
}
}
@ -241,9 +244,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
}
//Try to load vision via websocket
element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> {
startVisionUpdate(element, name, null, outputMeta)
}
// element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> {
// startVisionUpdate(element, name, null, outputMeta)
// }
else -> error("No embedded vision data / fetch url for $name")
}
@ -252,9 +255,13 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) {
listOf(
numberVisionRenderer(this),
textVisionRenderer(this),
formVisionRenderer(this)
htmlVisionRenderer,
inputVisionRenderer,
checkboxVisionRenderer,
numberVisionRenderer,
textVisionRenderer,
rangeVisionRenderer,
formVisionRenderer
).associateByName()
} else super<AbstractPlugin>.content(target)

View File

@ -2,66 +2,173 @@ package space.kscience.visionforge
import kotlinx.browser.document
import kotlinx.html.InputType
import kotlinx.html.div
import kotlinx.html.js.input
import kotlinx.html.js.label
import kotlinx.html.js.onChangeFunction
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import org.w3c.dom.get
import org.w3c.xhr.FormData
import space.kscience.dataforge.context.debug
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.DynamicMeta
import space.kscience.dataforge.meta.Meta
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
import space.kscience.dataforge.meta.*
import space.kscience.visionforge.html.*
internal fun textVisionRenderer(
client: JsVisionClient,
): ElementVisionRenderer = ElementVisionRenderer<VisionOfTextField> { name, vision, _ ->
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
vision.label?.let {
label {
htmlFor = fieldName
+it
/**
* Subscribes the HTML element to a given vision.
*
* @param vision The vision to subscribe to.
*/
private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
vision.useProperty(VisionOfHtml::classes) {
classList.value = classes.joinToString(separator = " ")
}
}
/**
* Subscribes the HTML input element to a given vision.
*
* @param inputVision The input vision to subscribe to.
*/
private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
subscribeToVision(inputVision)
inputVision.useProperty(VisionOfHtmlInput::disabled) {
disabled = it
}
}
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
this.name = fieldName
}.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) {
value = it ?: ""
}
onChangeFunction = {
client.notifyPropertyChanged(name, VisionOfTextField::text.name, value)
htmlInputElement.value = it ?: ""
}
}
}
}
internal fun numberVisionRenderer(
client: JsVisionClient,
): ElementVisionRenderer = ElementVisionRenderer<VisionOfNumberField> { name, vision, _ ->
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
vision.label?.let {
label {
htmlFor = fieldName
+it
}
}
internal val numberVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfNumberField> { _, vision, _ ->
input {
type = InputType.text
this.name = fieldName
}.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) {
value = it?.toDouble() ?: 0.0
}
onChangeFunction = {
client.notifyPropertyChanged(name, VisionOfNumberField::value.name, 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 {
@Suppress("UNUSED_VARIABLE") val formData = this
@ -86,17 +193,18 @@ internal fun FormData.toMeta(): Meta {
return DynamicMeta(`object`)
}
internal fun formVisionRenderer(
client: JsVisionClient,
): ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlForm> { name, vision, _ ->
internal val formVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlForm> { _, vision, _ ->
val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form")
client.logger.debug{"Adding hooks to form with id = '$vision.formId'"}
form.subscribeToVision(vision)
vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" }
vision.useProperty(VisionOfHtmlForm::values) { values ->
client.logger.debug{"Updating form '${vision.formId}' with values $values"}
vision.manager?.logger?.debug { "Updating form '${vision.formId}' with values $values" }
val inputs = form.getElementsByTagName("input")
values?.valueSequence()?.forEach { (token, value) ->
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
@ -106,8 +214,8 @@ internal fun formVisionRenderer(
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData)
vision.values = 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 space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.*
@ -17,7 +16,6 @@ import kotlin.random.nextUInt
/**
* A base class for different Jupyter VF integrations
*/
@DFExperimental
public abstract class VisionForgeIntegration(
public val visionManager: VisionManager,
) : JupyterIntegration(), ContextAware {

View File

@ -1,14 +1,14 @@
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 space.kscience.dataforge.context.Context
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.gdml.Gdml
import space.kscience.plotly.Plot
import space.kscience.plotly.PlotlyPage
import space.kscience.plotly.StaticPlotlyRenderer
import space.kscience.tables.*
import space.kscience.tables.Table
import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.VisionPage
@ -21,7 +21,6 @@ import space.kscience.visionforge.tables.toVision
import space.kscience.visionforge.visionManager
@DFExperimental
public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) {
override fun Builder.afterLoaded(vf: VisionForge) {

View File

@ -2,7 +2,7 @@ plugins {
id("space.kscience.gradle.mpp")
}
val plotlyVersion = "0.6.0"
val plotlyVersion = "0.6.1"
kscience {
jvm()

View File

@ -33,8 +33,8 @@ public class VisionOfPlotly private constructor(
@Transient
override val properties: MutableVisionProperties = object : MutableVisionProperties {
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
meta.setMeta(name, node)
override fun set(name: Name, node: Meta?, notify: Boolean) {
meta[name] = node
}
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 fun getMeta(
override fun get(
name: Name,
inherit: Boolean?,
includeStyles: Boolean?,
): MutableMeta = meta.getMeta(name) ?: MutableMeta()
): MutableMeta = meta[name] ?: MutableMeta()
override fun getValue(
name: Name,

View File

@ -32,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds
public class VisionRoute(
public val route: String,
public val visionManager: VisionManager,
override val meta: ObservableMutableMeta = MutableMeta(),
override val meta: ObservableMutableMeta = ObservableMutableMeta(),
) : Configurable, ContextAware {
public enum class Mode {

View File

@ -39,7 +39,7 @@ public inline fun MutableVisionContainer<Solid>.composite(
}
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)
return res

View File

@ -92,7 +92,7 @@ public class Extruded(
}
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?) {
if (value == null) {
thisRef.properties.setMeta(name, null)
thisRef.properties[name] = null
} else {
thisRef.properties[name + X_KEY] = value.x
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)
public var Solid.material: SolidMaterial?
get() = SolidMaterial.read(properties.getMeta(MATERIAL_KEY))
set(value) = properties.setMeta(MATERIAL_KEY, value?.meta)
get() = SolidMaterial.read(properties[MATERIAL_KEY])
set(value) = properties.set(MATERIAL_KEY, value?.meta)
@VisionBuilder
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?
@ -128,5 +128,5 @@ public var Solid.opacity: Number?
@VisionBuilder
public fun Solid.edges(enabled: Boolean = true, block: SolidMaterial.() -> Unit = {}) {
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 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(
name: Name,
@ -170,8 +170,8 @@ internal class SolidReferenceChild(
includeStyles: Boolean?,
): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles)
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
own.setMeta(name, node)
override fun set(name: Name, node: Meta?, notify: Boolean) {
own[name] = node
}
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 {
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(
getMeta(X_KEY).float ?: default,
getMeta(Y_KEY).float ?: default,
getMeta(Z_KEY).float ?: default
get(X_KEY).float ?: default,
get(Y_KEY).float ?: default,
get(Z_KEY).float ?: default
)

View File

@ -19,7 +19,7 @@ internal fun Solid.updateFrom(other: Solid): Solid {
scaleX *= other.scaleX
scaleY *= other.scaleY
scaleZ *= other.scaleZ
properties.setMeta(Name.EMPTY, other.properties.root())
properties[Name.EMPTY] = other.properties.root()
return this
}

View File

@ -28,7 +28,7 @@ internal class VisionUpdateTest {
propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
propertyChanged("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
}
targetVision.receiveChange(dif)
targetVision.update(dif)
assertTrue { targetVision.children.getChild("top") is SolidGroup }
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
assertEquals(

View File

@ -2,7 +2,7 @@ plugins {
id("space.kscience.gradle.mpp")
}
val tablesVersion = "0.2.1"
val tablesVersion = "0.3.0"
kscience {
jvm()

View File

@ -6,6 +6,7 @@ import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.int
import space.kscience.tables.ColumnHeader
import space.kscience.tables.ColumnTable
import space.kscience.tables.fill
import space.kscience.tables.get
import kotlin.math.pow
import kotlin.test.Test
@ -18,7 +19,7 @@ internal class VisionOfTableTest {
val y by ColumnHeader.typed<Value>()
val table = ColumnTable<Value>(100) {
x.fill { it.asValue() }
fill(x, null) { it.asValue() }
y.values = x.values.map { it?.double?.pow(2)?.asValue() }
}

View File

@ -1,6 +1,6 @@
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.startsWith
import space.kscience.visionforge.Vision
@ -17,7 +17,7 @@ import kotlin.reflect.KClass
/**
* Builder and updater for three.js object
*/
@Type(TYPE)
@DfType(TYPE)
public interface ThreeFactory<in T : Vision> {
public val type: KClass<in T>

View File

@ -24,7 +24,7 @@ public object ThreeLineFactory : ThreeFactory<PolyLine> {
}
val material = ThreeMaterials.getLineMaterial(
vision.properties.getMeta(SolidMaterial.MATERIAL_KEY),
vision.properties[SolidMaterial.MATERIAL_KEY],
false
)

View File

@ -83,7 +83,7 @@ public object ThreeMaterials {
private val visionMaterialCache = HashMap<Vision, Material>()
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
}
}
@ -133,11 +133,11 @@ public fun Mesh.setMaterial(vision: Vision) {
} else {
material = vision.parent?.let { parent ->
//TODO cache parent material
ThreeMaterials.buildMaterial(parent.properties.getMeta(SolidMaterial.MATERIAL_KEY))
ThreeMaterials.buildMaterial(parent.properties[SolidMaterial.MATERIAL_KEY])
} ?: ThreeMaterials.cacheMaterial(vision)
}
} 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) {
SolidMaterial.MATERIAL_COLOR_KEY -> {
material.asDynamic().color =
vision.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).threeColor()
vision.properties[SolidMaterial.MATERIAL_COLOR_KEY].threeColor()
?: ThreeMaterials.DEFAULT_COLOR
}
SolidMaterial.SPECULAR_COLOR_KEY -> {
material.asDynamic().specular =
vision.properties.getMeta(SolidMaterial.SPECULAR_COLOR_KEY).threeColor()
vision.properties[SolidMaterial.SPECULAR_COLOR_KEY].threeColor()
?: ThreeMaterials.DEFAULT_COLOR
}
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()
?: 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
//inherited edges definition, enabled by default
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) {
add(
LineSegments(

View File

@ -10,12 +10,14 @@ import space.kscience.dataforge.names.*
import space.kscience.visionforge.*
import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
import space.kscience.visionforge.solid.three.set
import three.core.Object3D
import kotlin.collections.set
import kotlin.reflect.KClass
import three.objects.Group as ThreeGroup
/**
* A plugin that handles Three Object3D representation of Visions.
*/
public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
override val tag: PluginTag get() = Companion.tag
@ -48,6 +50,13 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
as ThreeFactory<Solid>?
}
/**
* Build an Object3D representation of the given [Solid].
*
* @param vision [Solid] object to build a representation of;
* @param observe whether the constructed Object3D should be changed when the
* original [Vision] changes.
*/
public suspend fun buildObject3D(vision: Solid, observe: Boolean = true): Object3D = when (vision) {
is ThreeJsVision -> vision.render(this)
is SolidReference -> ThreeReferenceFactory.build(this, vision, observe)
@ -125,6 +134,16 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
private val canvasCache = HashMap<Element, ThreeCanvas>()
/**
* Return a [ThreeCanvas] object attached to the given [Element].
* If there is no canvas bound, a new canvas object is created
* and returned.
*
* @param element HTML element to which the canvas is
* (or should be if it is created by this call) attached;
* @param options canvas options that are applied to a newly
* created [ThreeCanvas] in case it does not exist.
*/
public fun getOrCreateCanvas(
element: Element,
options: Canvas3DOptions,
@ -142,6 +161,19 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
override fun rateVision(vision: Vision): Int =
if (vision is Solid) ElementVisionRenderer.DEFAULT_RATING else ElementVisionRenderer.ZERO_RATING
/**
* Render the given [Solid] Vision in a [ThreeCanvas] attached
* to the [element]. Canvas objects are cached, so subsequent calls
* with the same [element] value do not create new canvas objects,
* but they replace existing content, so multiple Visions cannot be
* displayed in a single [ThreeCanvas].
*
* @param element HTML element [ThreeCanvas] should be
* attached to;
* @param vision Vision to render;
* @param options options that are applied to a canvas
* in case it is not in the cache and should be created.
*/
internal fun renderSolid(
element: Element,
vision: Solid,
@ -165,6 +197,19 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
}
}
/**
* Render the given [Solid] Vision in a [ThreeCanvas] attached
* to the [element]. Canvas objects are cached, so subsequent calls
* with the same [element] value do not create new canvas objects,
* but they replace existing content, so multiple Visions cannot be
* displayed in a single [ThreeCanvas].
*
* @param element HTML element [ThreeCanvas] should be
* attached to;
* @param obj Vision to render;
* @param optionsBuilder option builder that is applied to a canvas
* in case it is not in the cache and should be created.
*/
public fun ThreePlugin.render(
element: HTMLElement,
obj: Solid,