forked from kscience/visionforge
Compare commits
12 Commits
ed71ba9ccb
...
c5c3868786
Author | SHA1 | Date | |
---|---|---|---|
c5c3868786 | |||
c0cf852c62 | |||
9fc6f1e34c | |||
595512959c | |||
fbb402de90 | |||
c877fcbce3 | |||
7561ddad36 | |||
469655092e | |||
80284a99ef | |||
e6bdb67262 | |||
71f7f59cb3 | |||
7b9fe54363 |
@ -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-16"
|
version = "0.3.0-dev-17"
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
@ -34,6 +34,6 @@ class GDMLVisionTest {
|
|||||||
val child = cubes[Name.of("composite-000","segment-0")]
|
val child = cubes[Name.of("composite-000","segment-0")]
|
||||||
assertNotNull(child)
|
assertNotNull(child)
|
||||||
child.properties.setValue(SolidMaterial.MATERIAL_COLOR_KEY, "red".asValue())
|
child.properties.setValue(SolidMaterial.MATERIAL_COLOR_KEY, "red".asValue())
|
||||||
assertEquals("red", child.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).string)
|
assertEquals("red", child.properties[SolidMaterial.MATERIAL_COLOR_KEY].string)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -71,7 +71,7 @@ class Model(val manager: VisionManager) {
|
|||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
map.values.forEach {
|
map.values.forEach {
|
||||||
it.properties.setMeta(SolidMaterial.MATERIAL_COLOR_KEY, null)
|
it.properties[SolidMaterial.MATERIAL_COLOR_KEY] = null
|
||||||
}
|
}
|
||||||
tracks.children.clear()
|
tracks.children.clear()
|
||||||
}
|
}
|
||||||
|
@ -54,9 +54,6 @@
|
|||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"jupyter": {
|
|
||||||
"outputs_hidden": false
|
|
||||||
},
|
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@ -83,9 +80,6 @@
|
|||||||
"language": "kotlin",
|
"language": "kotlin",
|
||||||
"name": "kotlin"
|
"name": "kotlin"
|
||||||
},
|
},
|
||||||
"ktnbPluginMetadata": {
|
|
||||||
"isAddProjectLibrariesToClasspath": false
|
|
||||||
},
|
|
||||||
"language_info": {
|
"language_info": {
|
||||||
"codemirror_mode": "text/x-kotlin",
|
"codemirror_mode": "text/x-kotlin",
|
||||||
"file_extension": ".kt",
|
"file_extension": ".kt",
|
||||||
@ -94,6 +88,9 @@
|
|||||||
"nbconvert_exporter": "",
|
"nbconvert_exporter": "",
|
||||||
"pygments_lexer": "kotlin",
|
"pygments_lexer": "kotlin",
|
||||||
"version": "1.8.20"
|
"version": "1.8.20"
|
||||||
|
},
|
||||||
|
"ktnbPluginMetadata": {
|
||||||
|
"projectLibraries": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
|
45
demo/playground/notebooks/controls.ipynb
Normal file
45
demo/playground/notebooks/controls.ipynb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"USE(JupyterCommonIntegration())"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"outputs": [],
|
||||||
|
"source": [],
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Kotlin",
|
||||||
|
"language": "kotlin",
|
||||||
|
"name": "kotlin"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "kotlin",
|
||||||
|
"version": "1.9.0",
|
||||||
|
"mimetype": "text/x-kotlin",
|
||||||
|
"file_extension": ".kt",
|
||||||
|
"pygments_lexer": "kotlin",
|
||||||
|
"codemirror_mode": "text/x-kotlin",
|
||||||
|
"nbconvert_exporter": ""
|
||||||
|
},
|
||||||
|
"ktnbPluginMetadata": {
|
||||||
|
"projectDependencies": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 0
|
||||||
|
}
|
@ -25,10 +25,7 @@
|
|||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"collapsed": false,
|
"collapsed": false
|
||||||
"jupyter": {
|
|
||||||
"outputs_hidden": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
@ -84,7 +81,7 @@
|
|||||||
"version": "1.8.0-dev-3517"
|
"version": "1.8.0-dev-3517"
|
||||||
},
|
},
|
||||||
"ktnbPluginMetadata": {
|
"ktnbPluginMetadata": {
|
||||||
"isAddProjectLibrariesToClasspath": false
|
"projectLibraries": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
|
@ -75,7 +75,7 @@ fun main() {
|
|||||||
|
|
||||||
server.openInBrowser()
|
server.openInBrowser()
|
||||||
|
|
||||||
while (readln() != "exit") {
|
while (readlnOrNull() != "exit") {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
docs/images/event-display-final.png
Normal file
BIN
docs/images/event-display-final.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
docs/images/event-display-selection.png
Normal file
BIN
docs/images/event-display-selection.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
416
docs/tutorials/tutorial-event-display.md
Normal file
416
docs/tutorials/tutorial-event-display.md
Normal 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")
|
@ -8,6 +8,6 @@ org.gradle.jvmargs=-Xmx4G
|
|||||||
|
|
||||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||||
|
|
||||||
toolsVersion=0.15.0-kotlin-1.9.20
|
toolsVersion=0.15.2-kotlin-1.9.21
|
||||||
#kotlin.experimental.tryK2=true
|
#kotlin.experimental.tryK2=true
|
||||||
#kscience.wasm.disabled=true
|
#kscience.wasm.disabled=true
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -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) {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.mpp")
|
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")
|
// id("com.android.library")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,14 +17,10 @@ import org.jetbrains.compose.web.dom.Text
|
|||||||
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.Name
|
import space.kscience.dataforge.names.*
|
||||||
import space.kscience.dataforge.names.NameToken
|
|
||||||
import space.kscience.dataforge.names.isEmpty
|
|
||||||
import space.kscience.dataforge.names.lastOrNull
|
|
||||||
import space.kscience.visionforge.hidden
|
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
|
* @param rootDescriptor Full path to the displayed node in [meta]. Could be empty
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun PropertyEditorItem(
|
public fun PropertyEditor(
|
||||||
/**
|
scope: CoroutineScope,
|
||||||
* Root config object - always non-null
|
|
||||||
*/
|
|
||||||
meta: MutableMeta,
|
meta: MutableMeta,
|
||||||
getPropertyState: (Name) -> EditorPropertyState,
|
getPropertyState: (Name) -> EditorPropertyState,
|
||||||
scope: CoroutineScope,
|
|
||||||
updates: Flow<Name>,
|
updates: Flow<Name>,
|
||||||
name: Name,
|
name: Name = Name.EMPTY,
|
||||||
rootDescriptor: MetaDescriptor?,
|
rootDescriptor: MetaDescriptor? = null,
|
||||||
initialExpanded: Boolean? = null,
|
initialExpanded: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) }
|
var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) }
|
||||||
@ -109,7 +103,7 @@ private fun PropertyEditorItem(
|
|||||||
Text(token)
|
Text(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) {
|
if (!name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) {
|
||||||
Div({
|
Div({
|
||||||
style {
|
style {
|
||||||
width(160.px)
|
width(160.px)
|
||||||
@ -145,7 +139,7 @@ private fun PropertyEditorItem(
|
|||||||
Div({
|
Div({
|
||||||
classes(TreeStyles.treeItem)
|
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,
|
descriptor: MetaDescriptor? = null,
|
||||||
expanded: Boolean? = null,
|
expanded: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
PropertyEditorItem(
|
PropertyEditor(
|
||||||
|
scope = scope,
|
||||||
meta = properties,
|
meta = properties,
|
||||||
getPropertyState = { name ->
|
getPropertyState = { name ->
|
||||||
if (properties[name] != null) {
|
if (properties[name] != null) {
|
||||||
@ -170,7 +165,6 @@ public fun PropertyEditor(
|
|||||||
EditorPropertyState.Undefined
|
EditorPropertyState.Undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scope = scope,
|
|
||||||
updates = callbackFlow {
|
updates = callbackFlow {
|
||||||
properties.onChange(scope) { name ->
|
properties.onChange(scope) { name ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -0,0 +1,102 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import org.jetbrains.compose.web.dom.*
|
||||||
|
import org.w3c.dom.HTMLDivElement
|
||||||
|
import org.w3c.dom.HTMLLIElement
|
||||||
|
|
||||||
|
|
||||||
|
public class ComposeTab(
|
||||||
|
public val key: String,
|
||||||
|
public val title: String,
|
||||||
|
public val content: ContentBuilder<HTMLDivElement>,
|
||||||
|
public val disabled: Boolean,
|
||||||
|
public val titleExt: ContentBuilder<HTMLLIElement>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Tabs(tabs: List<ComposeTab>, activeKey: String) {
|
||||||
|
var active by remember(activeKey) { mutableStateOf(activeKey) }
|
||||||
|
|
||||||
|
Div({ classes("card", "text-center") }) {
|
||||||
|
Div({ classes("card-header") }) {
|
||||||
|
|
||||||
|
Ul({ classes("nav", "nav-tabs", "card-header-tabs") }) {
|
||||||
|
tabs.forEach { tab ->
|
||||||
|
Li({
|
||||||
|
classes("nav-item")
|
||||||
|
}) {
|
||||||
|
A(attrs = {
|
||||||
|
classes("nav-link")
|
||||||
|
if (active == tab.key) {
|
||||||
|
classes("active")
|
||||||
|
}
|
||||||
|
if (tab.disabled) {
|
||||||
|
classes("disabled")
|
||||||
|
}
|
||||||
|
onClick {
|
||||||
|
active = tab.key
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(tab.title)
|
||||||
|
}
|
||||||
|
tab.titleExt.invoke(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabs.find { it.key == active }?.let { tab ->
|
||||||
|
Div({ classes("card-body") }) {
|
||||||
|
tab.content.invoke(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TabBuilder internal constructor(public val key: String) {
|
||||||
|
private var title: String = key
|
||||||
|
public var disabled: Boolean = false
|
||||||
|
private var content: ContentBuilder<HTMLDivElement> = {}
|
||||||
|
private var titleExt: ContentBuilder<HTMLLIElement> = {}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Content(content: ContentBuilder<HTMLDivElement>) {
|
||||||
|
this.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Title(title: String, titleExt: ContentBuilder<HTMLLIElement> = {}) {
|
||||||
|
this.title = title
|
||||||
|
this.titleExt = titleExt
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun build(): ComposeTab = ComposeTab(
|
||||||
|
key,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
disabled,
|
||||||
|
titleExt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TabsBuilder {
|
||||||
|
public var active: String = ""
|
||||||
|
internal val tabs: MutableList<ComposeTab> = mutableListOf()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Tab(key: String, builder: @Composable TabBuilder.() -> Unit) {
|
||||||
|
tabs.add(TabBuilder(key).apply { builder() }.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun addTab(tab: ComposeTab) {
|
||||||
|
tabs.add(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Tabs(builder: @Composable TabsBuilder.() -> Unit) {
|
||||||
|
val result = TabsBuilder().apply { builder() }
|
||||||
|
Tabs(result.tabs, result.active)
|
||||||
|
}
|
@ -0,0 +1,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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
@file:OptIn(ExperimentalComposeWebApi::class)
|
||||||
|
|
||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import app.softwork.bootstrapcompose.Card
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jetbrains.compose.web.ExperimentalComposeWebApi
|
||||||
|
import org.jetbrains.compose.web.css.*
|
||||||
|
import org.jetbrains.compose.web.dom.*
|
||||||
|
import space.kscience.dataforge.meta.get
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.isEmpty
|
||||||
|
import space.kscience.visionforge.*
|
||||||
|
import space.kscience.visionforge.solid.Solid
|
||||||
|
import space.kscience.visionforge.solid.SolidGroup
|
||||||
|
import space.kscience.visionforge.solid.Solids
|
||||||
|
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun ThreeCanvasWithControls(
|
||||||
|
solids: Solids,
|
||||||
|
builderOfSolid: Deferred<Solid?>,
|
||||||
|
initialSelected: Name?,
|
||||||
|
options: Canvas3DOptions?,
|
||||||
|
tabBuilder: @Composable TabsBuilder.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
var selected: Name? by remember { mutableStateOf(initialSelected) }
|
||||||
|
var solid: Solid? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(builderOfSolid) {
|
||||||
|
solids.context.launch {
|
||||||
|
solid = builderOfSolid.await()
|
||||||
|
//ensure that the solid is properly rooted
|
||||||
|
if (solid?.parent == null) {
|
||||||
|
solid?.setAsRoot(solids.context.visionManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val optionsWithSelector = remember(options) {
|
||||||
|
(options ?: Canvas3DOptions()).apply {
|
||||||
|
this.onSelect = {
|
||||||
|
selected = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedVision: Vision? = remember(builderOfSolid, selected) {
|
||||||
|
selected?.let {
|
||||||
|
when {
|
||||||
|
it.isEmpty() -> solid
|
||||||
|
else -> (solid as? SolidGroup)?.get(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FlexRow({
|
||||||
|
style {
|
||||||
|
height(100.percent)
|
||||||
|
width(100.percent)
|
||||||
|
flexWrap(FlexWrap.Wrap)
|
||||||
|
alignItems(AlignItems.Stretch)
|
||||||
|
alignContent(AlignContent.Stretch)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
FlexColumn({
|
||||||
|
style {
|
||||||
|
height(100.percent)
|
||||||
|
minWidth(600.px)
|
||||||
|
flex(10, 1, 600.px)
|
||||||
|
position(Position.Relative)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if (solid == null) {
|
||||||
|
Div({
|
||||||
|
style {
|
||||||
|
position(Position.Fixed)
|
||||||
|
width(100.percent)
|
||||||
|
height(100.percent)
|
||||||
|
zIndex(1000)
|
||||||
|
top(40.percent)
|
||||||
|
left(0.px)
|
||||||
|
opacity(0.5)
|
||||||
|
filter {
|
||||||
|
opacity(50.percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Div({ classes("d-flex", " justify-content-center") }) {
|
||||||
|
Div({
|
||||||
|
classes("spinner-grow", "text-primary")
|
||||||
|
style {
|
||||||
|
width(3.cssRem)
|
||||||
|
height(3.cssRem)
|
||||||
|
zIndex(20)
|
||||||
|
}
|
||||||
|
attr("role", "status")
|
||||||
|
}) {
|
||||||
|
Span({ classes("sr-only") }) { Text("Loading 3D vision") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ThreeCanvas(solids.context, optionsWithSelector, solid, selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedVision?.let { vision ->
|
||||||
|
Div({
|
||||||
|
style {
|
||||||
|
position(Position.Absolute)
|
||||||
|
top(5.px)
|
||||||
|
right(5.px)
|
||||||
|
width(450.px)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Card(
|
||||||
|
headerAttrs = {
|
||||||
|
// border = true
|
||||||
|
},
|
||||||
|
header = {
|
||||||
|
NameCrumbs(selected) { selected = it }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
PropertyEditor(
|
||||||
|
scope = solids.context,
|
||||||
|
meta = vision.properties.root(),
|
||||||
|
getPropertyState = { name ->
|
||||||
|
if (vision.properties.own?.get(name) != null) {
|
||||||
|
EditorPropertyState.Defined
|
||||||
|
} else if (vision.properties.root()[name] != null) {
|
||||||
|
// TODO differentiate
|
||||||
|
EditorPropertyState.Default()
|
||||||
|
} else {
|
||||||
|
EditorPropertyState.Undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updates = vision.properties.changes,
|
||||||
|
rootDescriptor = vision.descriptor
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
vision.styles.takeIf { it.isNotEmpty() }?.let { styles ->
|
||||||
|
P {
|
||||||
|
B { Text("Styles: ") }
|
||||||
|
Text(styles.joinToString(separator = ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlexColumn({
|
||||||
|
style {
|
||||||
|
paddingAll(4.px)
|
||||||
|
minWidth(400.px)
|
||||||
|
height(100.percent)
|
||||||
|
overflowY("auto")
|
||||||
|
flex(1, 10, 300.px)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
ThreeControls(solid, optionsWithSelector, selected, onSelect = { selected = it }, tabBuilder = tabBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import org.jetbrains.compose.web.css.Color
|
||||||
|
import org.jetbrains.compose.web.css.color
|
||||||
|
import org.jetbrains.compose.web.css.cursor
|
||||||
|
import org.jetbrains.compose.web.css.textDecorationLine
|
||||||
|
import org.jetbrains.compose.web.dom.Div
|
||||||
|
import org.jetbrains.compose.web.dom.Span
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.lastOrNull
|
||||||
|
import space.kscience.dataforge.names.plus
|
||||||
|
import space.kscience.dataforge.names.startsWith
|
||||||
|
import space.kscience.visionforge.Vision
|
||||||
|
import space.kscience.visionforge.VisionGroup
|
||||||
|
import space.kscience.visionforge.asSequence
|
||||||
|
import space.kscience.visionforge.compose.TreeStyles.hover
|
||||||
|
import space.kscience.visionforge.compose.TreeStyles.invoke
|
||||||
|
import space.kscience.visionforge.isEmpty
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TreeLabel(
|
||||||
|
vision: Vision,
|
||||||
|
name: Name,
|
||||||
|
selected: Name?,
|
||||||
|
clickCallback: (Name) -> Unit,
|
||||||
|
) {
|
||||||
|
Span({
|
||||||
|
classes(TreeStyles.treeLabel)
|
||||||
|
if (name == selected) {
|
||||||
|
classes(TreeStyles.treeLabelSelected)
|
||||||
|
}
|
||||||
|
style {
|
||||||
|
color(Color("#069"))
|
||||||
|
cursor("pointer")
|
||||||
|
hover.invoke {
|
||||||
|
textDecorationLine("underline")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
onClick { clickCallback(name) }
|
||||||
|
}) {
|
||||||
|
Text(name.lastOrNull()?.toString() ?: "World")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun VisionTree(
|
||||||
|
vision: Vision,
|
||||||
|
name: Name = Name.EMPTY,
|
||||||
|
selected: Name? = null,
|
||||||
|
clickCallback: (Name) -> Unit,
|
||||||
|
): Unit {
|
||||||
|
var expanded: Boolean by remember { mutableStateOf(selected?.startsWith(name) ?: false) }
|
||||||
|
|
||||||
|
//display as node if any child is visible
|
||||||
|
if (vision is VisionGroup) {
|
||||||
|
FlexRow {
|
||||||
|
if (vision.children.keys.any { !it.body.startsWith("@") }) {
|
||||||
|
Span({
|
||||||
|
classes(TreeStyles.treeCaret)
|
||||||
|
if (expanded) {
|
||||||
|
classes(TreeStyles.treeCaretDown)
|
||||||
|
}
|
||||||
|
onClick {
|
||||||
|
expanded = !expanded
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
TreeLabel(vision, name, selected, clickCallback)
|
||||||
|
}
|
||||||
|
if (expanded) {
|
||||||
|
FlexColumn({
|
||||||
|
classes(TreeStyles.tree)
|
||||||
|
}) {
|
||||||
|
vision.children.asSequence()
|
||||||
|
.filter { !it.first.toString().startsWith("@") } // ignore statics and other hidden children
|
||||||
|
.sortedBy { (it.second as? VisionGroup)?.children?.isEmpty() ?: true } // ignore empty groups
|
||||||
|
.forEach { (childToken, child) ->
|
||||||
|
Div({ classes(TreeStyles.treeItem) }) {
|
||||||
|
VisionTree(
|
||||||
|
child,
|
||||||
|
name + childToken,
|
||||||
|
selected,
|
||||||
|
clickCallback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TreeLabel(vision, name, selected, clickCallback)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import org.jetbrains.compose.web.dom.H5
|
||||||
|
import org.jetbrains.compose.web.dom.Text
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun CardTitle(title: String): Unit = H5({ classes("card-title") }) { Text(title) }
|
@ -1,6 +1,7 @@
|
|||||||
package space.kscience.visionforge.compose
|
package space.kscience.visionforge.compose
|
||||||
|
|
||||||
import org.jetbrains.compose.web.css.*
|
import org.jetbrains.compose.web.css.*
|
||||||
|
import org.jetbrains.compose.web.css.keywords.CSSAutoKeyword
|
||||||
|
|
||||||
public enum class UserSelect {
|
public enum class UserSelect {
|
||||||
inherit, initial, revert, revertLayer, unset,
|
inherit, initial, revert, revertLayer, unset,
|
||||||
@ -33,3 +34,11 @@ public fun StyleScope.marginAll(
|
|||||||
) {
|
) {
|
||||||
margin(top, right, bottom, left)
|
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)
|
||||||
|
}
|
@ -16,7 +16,7 @@ import org.w3c.dom.HTMLOptionElement
|
|||||||
import org.w3c.dom.asList
|
import org.w3c.dom.asList
|
||||||
import space.kscience.dataforge.meta.*
|
import space.kscience.dataforge.meta.*
|
||||||
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.allowedValues
|
import space.kscience.dataforge.meta.descriptors.allowedValues
|
||||||
import space.kscience.visionforge.Colors
|
import space.kscience.visionforge.Colors
|
||||||
import space.kscience.visionforge.widgetType
|
import space.kscience.visionforge.widgetType
|
||||||
@ -199,7 +199,7 @@ public fun RangeValueChooser(
|
|||||||
|
|
||||||
|
|
||||||
FlexRow {
|
FlexRow {
|
||||||
if (descriptor?.valueRequirement != ValueRequirement.REQUIRED) {
|
if (descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
|
||||||
Input(type = InputType.Checkbox) {
|
Input(type = InputType.Checkbox) {
|
||||||
if (!rangeDisabled) defaultChecked()
|
if (!rangeDisabled) defaultChecked()
|
||||||
|
|
||||||
|
@ -16,9 +16,8 @@ import react.dom.attrs
|
|||||||
import space.kscience.dataforge.meta.MutableMeta
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
import space.kscience.dataforge.meta.ObservableMutableMeta
|
import space.kscience.dataforge.meta.ObservableMutableMeta
|
||||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
import space.kscience.dataforge.meta.descriptors.ValueRequirement
|
import space.kscience.dataforge.meta.descriptors.ValueRestriction
|
||||||
import space.kscience.dataforge.meta.descriptors.get
|
import space.kscience.dataforge.meta.descriptors.get
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.remove
|
import space.kscience.dataforge.meta.remove
|
||||||
import space.kscience.dataforge.names.*
|
import space.kscience.dataforge.names.*
|
||||||
import space.kscience.visionforge.hidden
|
import space.kscience.visionforge.hidden
|
||||||
@ -146,7 +145,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
|
|||||||
}
|
}
|
||||||
+token
|
+token
|
||||||
}
|
}
|
||||||
if (!props.name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) {
|
if (!props.name.isEmpty() && descriptor?.valueRestriction != ValueRestriction.ABSENT) {
|
||||||
styledDiv {
|
styledDiv {
|
||||||
css {
|
css {
|
||||||
//+TreeStyles.resizeableInput
|
//+TreeStyles.resizeableInput
|
||||||
@ -185,7 +184,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
|
|||||||
}
|
}
|
||||||
+"\u00D7"
|
+"\u00D7"
|
||||||
attrs {
|
attrs {
|
||||||
if (editorPropertyState!= EditorPropertyState.Defined) {
|
if (editorPropertyState != EditorPropertyState.Defined) {
|
||||||
disabled = true
|
disabled = true
|
||||||
} else {
|
} else {
|
||||||
onClickFunction = removeClick
|
onClickFunction = removeClick
|
||||||
|
@ -12,7 +12,7 @@ import react.dom.attrs
|
|||||||
import react.fc
|
import react.fc
|
||||||
import react.useState
|
import react.useState
|
||||||
import space.kscience.dataforge.meta.asValue
|
import space.kscience.dataforge.meta.asValue
|
||||||
import space.kscience.dataforge.meta.descriptors.ValueRequirement
|
import space.kscience.dataforge.meta.descriptors.ValueRestriction
|
||||||
import space.kscience.dataforge.meta.double
|
import space.kscience.dataforge.meta.double
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
@ -43,7 +43,7 @@ public val RangeValueChooser: FC<ValueChooserProps> = fc("RangeValueChooser") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flexRow {
|
flexRow {
|
||||||
if (props.descriptor?.valueRequirement != ValueRequirement.REQUIRED) {
|
if (props.descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
|
||||||
styledInput(type = InputType.checkBox) {
|
styledInput(type = InputType.checkBox) {
|
||||||
attrs {
|
attrs {
|
||||||
defaultChecked = rangeDisabled.not()
|
defaultChecked = rangeDisabled.not()
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package space.kscience.visionforge
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.MetaRepr
|
||||||
|
import space.kscience.dataforge.meta.MutableMeta
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("control")
|
||||||
|
public abstract class VisionControlEvent : VisionEvent, MetaRepr {
|
||||||
|
public abstract val meta: Meta
|
||||||
|
|
||||||
|
override fun toMeta(): Meta = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ControlVision : Vision {
|
||||||
|
public val controlEventFlow: SharedFlow<VisionControlEvent>
|
||||||
|
|
||||||
|
public fun dispatchControlEvent(event: VisionControlEvent)
|
||||||
|
|
||||||
|
override fun receiveEvent(event: VisionEvent) {
|
||||||
|
if (event is VisionControlEvent) {
|
||||||
|
dispatchControlEvent(event)
|
||||||
|
} else super.receiveEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param payload The optional payload associated with the click event.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("control.click")
|
||||||
|
public class VisionClickEvent(public val payload: Meta = Meta.EMPTY) : VisionControlEvent() {
|
||||||
|
override val meta: Meta get() = Meta { ::payload.name put payload }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface ClickControl : ControlVision {
|
||||||
|
/**
|
||||||
|
* Create and dispatch a click event
|
||||||
|
*/
|
||||||
|
public fun click(builder: MutableMeta.() -> Unit = {}) {
|
||||||
|
dispatchControlEvent(VisionClickEvent(Meta(builder)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register listener
|
||||||
|
*/
|
||||||
|
public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job =
|
||||||
|
controlEventFlow.filterIsInstance<VisionClickEvent>().onEach(block).launchIn(scope)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("control.valueChange")
|
||||||
|
public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent()
|
@ -13,7 +13,7 @@ import kotlin.jvm.JvmInline
|
|||||||
@JvmInline
|
@JvmInline
|
||||||
public value class StyleSheet(private val owner: Vision) {
|
public value class StyleSheet(private val owner: Vision) {
|
||||||
|
|
||||||
private val styleNode: Meta get() = owner.properties.getMeta(STYLESHEET_KEY)
|
private val styleNode: Meta get() = owner.properties[STYLESHEET_KEY]
|
||||||
|
|
||||||
public val items: Map<NameToken, Meta> get() = styleNode.items
|
public val items: Map<NameToken, Meta> get() = styleNode.items
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ public value class StyleSheet(private val owner: Vision) {
|
|||||||
* Define a style without notifying owner
|
* Define a style without notifying owner
|
||||||
*/
|
*/
|
||||||
public fun define(key: String, style: Meta?) {
|
public fun define(key: String, style: Meta?) {
|
||||||
owner.properties.setMeta(STYLESHEET_KEY + key, style)
|
owner.properties[STYLESHEET_KEY + key] = style
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,7 +92,7 @@ public fun Vision.useStyle(name: String, notify: Boolean = true) {
|
|||||||
* Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision].
|
* Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision].
|
||||||
*/
|
*/
|
||||||
public fun Vision.getStyle(name: String): Meta? =
|
public fun Vision.getStyle(name: String): Meta? =
|
||||||
properties.own?.getMeta(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name)
|
properties.own?.get(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a property from all styles
|
* Resolve a property from all styles
|
||||||
|
@ -4,12 +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.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.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
|
||||||
@ -18,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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,7 +38,7 @@ public interface Vision : Described {
|
|||||||
/**
|
/**
|
||||||
* Update this vision using a dif represented by [VisionChange].
|
* Update this vision using a dif represented by [VisionChange].
|
||||||
*/
|
*/
|
||||||
public fun receiveChange(change: VisionChange) {
|
public fun update(change: VisionChange) {
|
||||||
if (change.children?.isNotEmpty() == true) {
|
if (change.children?.isNotEmpty() == true) {
|
||||||
error("Vision is not a group")
|
error("Vision is not a group")
|
||||||
}
|
}
|
||||||
@ -46,18 +47,12 @@ public interface Vision : Described {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun onMetaEvent(meta: Meta){
|
|
||||||
//Do nothing by default
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receive and process a generic [VisionEvent].
|
* Receive and process a generic [VisionEvent].
|
||||||
*/
|
*/
|
||||||
public fun receiveEvent(event: VisionEvent) {
|
public fun receiveEvent(event: VisionEvent) {
|
||||||
when (event) {
|
if(event is VisionChange) update(event)
|
||||||
is VisionChange -> receiveChange(event)
|
else manager?.logger?.warn { "Undispatched event: $event" }
|
||||||
is VisionMetaEvent -> onMetaEvent(event.meta)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val descriptor: MetaDescriptor?
|
override val descriptor: MetaDescriptor?
|
||||||
|
@ -18,7 +18,6 @@ public interface VisionClient: Plugin {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,17 @@ import space.kscience.visionforge.VisionChildren.Companion.STATIC_TOKEN_BODY
|
|||||||
@DslMarker
|
@DslMarker
|
||||||
public annotation class VisionBuilder
|
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 interface VisionContainer<out V : Vision> {
|
||||||
public fun getChild(name: Name): V?
|
public fun getChild(name: Name): V?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container interface with write/replace/delete access to its content.
|
||||||
|
*/
|
||||||
public interface MutableVisionContainer<in V : Vision> {
|
public interface MutableVisionContainer<in V : Vision> {
|
||||||
//TODO add documentation
|
//TODO add documentation
|
||||||
public fun setChild(name: Name?, child: V?)
|
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)!!) }
|
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 interface MutableVisionChildren : VisionChildren, MutableVisionContainer<Vision> {
|
||||||
|
|
||||||
public override val parent: MutableVisionGroup
|
public override val parent: MutableVisionGroup
|
||||||
|
|
||||||
public operator fun set(token: NameToken, value: Vision?)
|
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?) {
|
override fun setChild(name: Name?, child: Vision?) {
|
||||||
when {
|
when {
|
||||||
name == null -> {
|
name == null -> {
|
||||||
|
@ -3,8 +3,6 @@ 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,13 +21,3 @@ public sealed interface VisionEvent {
|
|||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("meta")
|
@SerialName("meta")
|
||||||
public class VisionMetaEvent(public val meta: Meta) : VisionEvent
|
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)
|
|
||||||
}
|
|
@ -17,12 +17,12 @@ import space.kscience.visionforge.Vision.Companion.STYLE_KEY
|
|||||||
public interface VisionGroup : Vision {
|
public interface VisionGroup : Vision {
|
||||||
public val children: VisionChildren
|
public val children: VisionChildren
|
||||||
|
|
||||||
override fun receiveChange(change: VisionChange) {
|
override fun update(change: VisionChange) {
|
||||||
change.children?.forEach { (name, change) ->
|
change.children?.forEach { (name, change) ->
|
||||||
if (change.vision != null || change.vision == NullVision) {
|
if (change.vision != null || change.vision == NullVision) {
|
||||||
error("VisionGroup is read-only")
|
error("VisionGroup is read-only")
|
||||||
} else {
|
} else {
|
||||||
children.getChild(name)?.receiveChange(change)
|
children.getChild(name)?.update(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
change.properties?.let {
|
change.properties?.let {
|
||||||
@ -37,12 +37,12 @@ public interface MutableVisionGroup : VisionGroup {
|
|||||||
|
|
||||||
public fun createGroup(): MutableVisionGroup
|
public fun createGroup(): MutableVisionGroup
|
||||||
|
|
||||||
override fun receiveChange(change: VisionChange) {
|
override fun update(change: VisionChange) {
|
||||||
change.children?.forEach { (name, change) ->
|
change.children?.forEach { (name, change) ->
|
||||||
when {
|
when {
|
||||||
change.vision == NullVision -> children.setChild(name, null)
|
change.vision == NullVision -> children.setChild(name, null)
|
||||||
change.vision != null -> children.setChild(name, change.vision)
|
change.vision != null -> children.setChild(name, change.vision)
|
||||||
else -> children.getChild(name)?.receiveChange(change)
|
else -> children.getChild(name)?.update(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
change.properties?.let {
|
change.properties?.let {
|
||||||
|
@ -13,10 +13,7 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
|||||||
import space.kscience.dataforge.meta.toJson
|
import space.kscience.dataforge.meta.toJson
|
||||||
import space.kscience.dataforge.meta.toMeta
|
import space.kscience.dataforge.meta.toMeta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.visionforge.html.VisionOfCheckbox
|
import space.kscience.visionforge.html.*
|
||||||
import space.kscience.visionforge.html.VisionOfHtmlForm
|
|
||||||
import space.kscience.visionforge.html.VisionOfNumberField
|
|
||||||
import space.kscience.visionforge.html.VisionOfTextField
|
|
||||||
|
|
||||||
public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionContainer<Vision> {
|
public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionContainer<Vision> {
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
@ -72,9 +69,11 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont
|
|||||||
defaultDeserializer { SimpleVisionGroup.serializer() }
|
defaultDeserializer { SimpleVisionGroup.serializer() }
|
||||||
subclass(NullVision.serializer())
|
subclass(NullVision.serializer())
|
||||||
subclass(SimpleVisionGroup.serializer())
|
subclass(SimpleVisionGroup.serializer())
|
||||||
|
subclass(VisionOfHtmlInput.serializer())
|
||||||
subclass(VisionOfNumberField.serializer())
|
subclass(VisionOfNumberField.serializer())
|
||||||
subclass(VisionOfTextField.serializer())
|
subclass(VisionOfTextField.serializer())
|
||||||
subclass(VisionOfCheckbox.serializer())
|
subclass(VisionOfCheckbox.serializer())
|
||||||
|
subclass(VisionOfRangeField.serializer())
|
||||||
subclass(VisionOfHtmlForm.serializer())
|
subclass(VisionOfHtmlForm.serializer())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,13 +34,13 @@ public interface VisionProperties : MetaProvider {
|
|||||||
* @param inherit toggles parent node property lookup. Null means inference from descriptor.
|
* @param inherit toggles parent node property lookup. Null means inference from descriptor.
|
||||||
* @param includeStyles toggles inclusion of properties from styles.
|
* @param includeStyles toggles inclusion of properties from styles.
|
||||||
*/
|
*/
|
||||||
public fun getMeta(
|
public fun get(
|
||||||
name: Name,
|
name: Name,
|
||||||
inherit: Boolean?,
|
inherit: Boolean?,
|
||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): Meta
|
): Meta
|
||||||
|
|
||||||
override fun getMeta(name: Name): Meta? = getMeta(name, null, null)
|
override fun get(name: Name): Meta? = get(name, null, null)
|
||||||
|
|
||||||
|
|
||||||
public val changes: Flow<Name>
|
public val changes: Flow<Name>
|
||||||
@ -54,7 +54,7 @@ public interface VisionProperties : MetaProvider {
|
|||||||
|
|
||||||
public interface MutableVisionProperties : VisionProperties, MutableMetaProvider {
|
public interface MutableVisionProperties : VisionProperties, MutableMetaProvider {
|
||||||
|
|
||||||
override fun getMeta(
|
override fun get(
|
||||||
name: Name,
|
name: Name,
|
||||||
inherit: Boolean?,
|
inherit: Boolean?,
|
||||||
includeStyles: Boolean?,
|
includeStyles: Boolean?,
|
||||||
@ -65,7 +65,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
|
|||||||
includeStyles,
|
includeStyles,
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun setMeta(
|
public fun set(
|
||||||
name: Name,
|
name: Name,
|
||||||
node: Meta?,
|
node: Meta?,
|
||||||
notify: Boolean,
|
notify: Boolean,
|
||||||
@ -77,10 +77,10 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
|
|||||||
notify: Boolean,
|
notify: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getMeta(name: Name): MutableMeta = getMeta(name, null, null)
|
override fun get(name: Name): MutableMeta = get(name, null, null)
|
||||||
|
|
||||||
override fun setMeta(name: Name, node: Meta?) {
|
override fun set(name: Name, node: Meta?) {
|
||||||
setMeta(name, node, true)
|
set(name, node, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setValue(name: Name, value: Value?) {
|
override fun setValue(name: Name, value: Value?) {
|
||||||
@ -89,7 +89,7 @@ public interface MutableVisionProperties : VisionProperties, MutableMetaProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun MutableVisionProperties.remove(name: Name) {
|
public fun MutableVisionProperties.remove(name: Name) {
|
||||||
setMeta(name, null)
|
set(name, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun MutableVisionProperties.remove(name: String) {
|
public fun MutableVisionProperties.remove(name: String) {
|
||||||
@ -114,7 +114,7 @@ private class VisionPropertiesItem(
|
|||||||
|
|
||||||
override val items: Map<NameToken, MutableMeta>
|
override val items: Map<NameToken, MutableMeta>
|
||||||
get() {
|
get() {
|
||||||
val metaKeys = properties.own?.getMeta(nodeName)?.items?.keys ?: emptySet()
|
val metaKeys = properties.own?.get(nodeName)?.items?.keys ?: emptySet()
|
||||||
val descriptorKeys = descriptor?.children?.map { NameToken(it.key) } ?: emptySet()
|
val descriptorKeys = descriptor?.children?.map { NameToken(it.key) } ?: emptySet()
|
||||||
val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet()
|
val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet()
|
||||||
val inheritFlag = descriptor?.inherited ?: inherit
|
val inheritFlag = descriptor?.inherited ?: inherit
|
||||||
@ -148,8 +148,8 @@ private class VisionPropertiesItem(
|
|||||||
default
|
default
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun setMeta(name: Name, node: Meta?) {
|
override fun set(name: Name, node: Meta?) {
|
||||||
properties.setMeta(nodeName + name, node)
|
properties[nodeName + name] = node
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = Meta.toString(this)
|
override fun toString(): String = Meta.toString(this)
|
||||||
@ -202,16 +202,16 @@ public abstract class AbstractVisionProperties(
|
|||||||
return descriptor?.defaultValue
|
return descriptor?.defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
|
override fun set(name: Name, node: Meta?, notify: Boolean) {
|
||||||
//ignore if the value is the same as existing
|
//ignore if the value is the same as existing
|
||||||
if (own?.getMeta(name) == node) return
|
if (own?.get(name) == node) return
|
||||||
|
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
properties = node?.asMutableMeta()
|
properties = node?.asMutableMeta()
|
||||||
} else if (node == null) {
|
} else if (node == null) {
|
||||||
properties?.setMeta(name, node)
|
properties?.set(name, node)
|
||||||
} else {
|
} else {
|
||||||
getOrCreateProperties().setMeta(name, node)
|
getOrCreateProperties()[name] = node
|
||||||
}
|
}
|
||||||
if (notify) {
|
if (notify) {
|
||||||
invalidate(name)
|
invalidate(name)
|
||||||
@ -223,7 +223,7 @@ public abstract class AbstractVisionProperties(
|
|||||||
if (own?.getValue(name) == value) return
|
if (own?.getValue(name) == value) return
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
properties?.getMeta(name)?.value = null
|
properties?.get(name)?.value = null
|
||||||
} else {
|
} else {
|
||||||
getOrCreateProperties().setValue(name, value)
|
getOrCreateProperties().setValue(name, value)
|
||||||
}
|
}
|
||||||
@ -272,11 +272,11 @@ public fun VisionProperties.getValue(
|
|||||||
/**
|
/**
|
||||||
* Get [Vision] property using key as a String
|
* Get [Vision] property using key as a String
|
||||||
*/
|
*/
|
||||||
public fun VisionProperties.getMeta(
|
public fun VisionProperties.get(
|
||||||
name: String,
|
name: String,
|
||||||
inherit: Boolean? = null,
|
inherit: Boolean? = null,
|
||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): Meta = getMeta(name.parseAsName(), inherit, includeStyles)
|
): Meta = get(name.parseAsName(), inherit, includeStyles)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The root property node with given inheritance and style flags
|
* The root property node with given inheritance and style flags
|
||||||
@ -286,17 +286,17 @@ public fun VisionProperties.getMeta(
|
|||||||
public fun MutableVisionProperties.root(
|
public fun MutableVisionProperties.root(
|
||||||
inherit: Boolean? = null,
|
inherit: Boolean? = null,
|
||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): MutableMeta = getMeta(Name.EMPTY, inherit, includeStyles)
|
): MutableMeta = get(Name.EMPTY, inherit, includeStyles)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get [Vision] property using key as a String
|
* Get [Vision] property using key as a String
|
||||||
*/
|
*/
|
||||||
public fun MutableVisionProperties.getMeta(
|
public fun MutableVisionProperties.get(
|
||||||
name: String,
|
name: String,
|
||||||
inherit: Boolean? = null,
|
inherit: Boolean? = null,
|
||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): MutableMeta = getMeta(name.parseAsName(), inherit, includeStyles)
|
): MutableMeta = get(name.parseAsName(), inherit, includeStyles)
|
||||||
|
|
||||||
//
|
//
|
||||||
//public operator fun MutableVisionProperties.set(name: Name, value: Number): Unit =
|
//public operator fun MutableVisionProperties.set(name: Name, value: Number): Unit =
|
||||||
|
@ -17,10 +17,10 @@ public fun Vision.flowProperty(
|
|||||||
includeStyles: Boolean? = null,
|
includeStyles: Boolean? = null,
|
||||||
): Flow<Meta> = flow {
|
): Flow<Meta> = flow {
|
||||||
//Pass initial value.
|
//Pass initial value.
|
||||||
emit(properties.getMeta(propertyName, inherit, includeStyles))
|
emit(properties.get(propertyName, inherit, includeStyles))
|
||||||
properties.changes.collect { name ->
|
properties.changes.collect { name ->
|
||||||
if (name.startsWith(propertyName)) {
|
if (name.startsWith(propertyName)) {
|
||||||
emit(properties.getMeta(propertyName, inherit, includeStyles))
|
emit(properties.get(propertyName, inherit, includeStyles))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
package space.kscience.visionforge.html
|
||||||
|
|
||||||
|
import kotlinx.html.InputType
|
||||||
|
import kotlinx.html.TagConsumer
|
||||||
|
import kotlinx.html.stream.createHTML
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import space.kscience.dataforge.meta.*
|
||||||
|
import space.kscience.dataforge.names.asName
|
||||||
|
import space.kscience.visionforge.AbstractVision
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public abstract class VisionOfHtml : AbstractVision() {
|
||||||
|
public var classes: List<String> by properties.stringList(*emptyArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.plain")
|
||||||
|
public class VisionOfPlainHtml : VisionOfHtml() {
|
||||||
|
public var content: String? by properties.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) {
|
||||||
|
content = createHTML().apply(block).finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.html(
|
||||||
|
block: VisionOfPlainHtml.() -> Unit,
|
||||||
|
): VisionOfPlainHtml = VisionOfPlainHtml().apply(block)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
public enum class InputFeedbackMode {
|
||||||
|
/**
|
||||||
|
* Fire feedback event on `onchange` event
|
||||||
|
*/
|
||||||
|
ONCHANGE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire feedback event on `oninput` event
|
||||||
|
*/
|
||||||
|
ONINPUT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* provide only manual feedback
|
||||||
|
*/
|
||||||
|
NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.input")
|
||||||
|
public open class VisionOfHtmlInput(
|
||||||
|
public val inputType: String,
|
||||||
|
public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE,
|
||||||
|
) : VisionOfHtml() {
|
||||||
|
public var value: Value? by properties.value()
|
||||||
|
public var disabled: Boolean by properties.boolean { false }
|
||||||
|
public var fieldName: String? by properties.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlInput(
|
||||||
|
inputType: String,
|
||||||
|
block: VisionOfHtmlInput.() -> Unit = {},
|
||||||
|
): VisionOfHtmlInput = VisionOfHtmlInput(inputType).apply(block)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.text")
|
||||||
|
public class VisionOfTextField : VisionOfHtmlInput(InputType.text.realValue) {
|
||||||
|
public var text: String? by properties.string(key = VisionOfHtmlInput::value.name.asName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlTextField(
|
||||||
|
block: VisionOfTextField.() -> Unit = {},
|
||||||
|
): VisionOfTextField = VisionOfTextField().apply(block)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.checkbox")
|
||||||
|
public class VisionOfCheckbox : VisionOfHtmlInput(InputType.checkBox.realValue) {
|
||||||
|
public var checked: Boolean? by properties.boolean(key = VisionOfHtmlInput::value.name.asName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlCheckBox(
|
||||||
|
block: VisionOfCheckbox.() -> Unit = {},
|
||||||
|
): VisionOfCheckbox = VisionOfCheckbox().apply(block)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.number")
|
||||||
|
public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) {
|
||||||
|
public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlNumberField(
|
||||||
|
block: VisionOfNumberField.() -> Unit = {},
|
||||||
|
): VisionOfNumberField = VisionOfNumberField().apply(block)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.range")
|
||||||
|
public class VisionOfRangeField(
|
||||||
|
public val min: Double,
|
||||||
|
public val max: Double,
|
||||||
|
public val step: Double = 1.0,
|
||||||
|
) : VisionOfHtmlInput(InputType.range.realValue) {
|
||||||
|
public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
public inline fun VisionOutput.htmlRangeField(
|
||||||
|
min: Double,
|
||||||
|
max: Double,
|
||||||
|
step: Double = 1.0,
|
||||||
|
block: VisionOfRangeField.() -> Unit = {},
|
||||||
|
): VisionOfRangeField = VisionOfRangeField(min, max, step).apply(block)
|
||||||
|
|
@ -9,12 +9,15 @@ import kotlinx.serialization.Serializable
|
|||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.node
|
import space.kscience.dataforge.meta.node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param formId an id of the element in rendered DOM, this form is bound to
|
||||||
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("html.form")
|
@SerialName("html.form")
|
||||||
public class VisionOfHtmlForm(
|
public class VisionOfHtmlForm(
|
||||||
public val formId: String,
|
public val formId: String,
|
||||||
) : VisionOfHtmlInput() {
|
) : VisionOfHtml() {
|
||||||
public var values: Meta? by mutableProperties.node()
|
public var values: Meta? by properties.node()
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <R> TagConsumer<R>.bindForm(
|
public fun <R> TagConsumer<R>.bindForm(
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
package space.kscience.visionforge.html
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import space.kscience.dataforge.meta.boolean
|
|
||||||
import space.kscience.dataforge.meta.number
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import space.kscience.dataforge.names.Name
|
|
||||||
import space.kscience.visionforge.AbstractVision
|
|
||||||
import space.kscience.visionforge.Vision
|
|
||||||
|
|
||||||
//TODO replace by something
|
|
||||||
internal val Vision.mutableProperties get() = properties.getMeta(Name.EMPTY, false, false)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
public abstract class VisionOfHtmlInput : AbstractVision() {
|
|
||||||
public var disabled: Boolean by mutableProperties.boolean { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@SerialName("html.text")
|
|
||||||
public class VisionOfTextField(
|
|
||||||
public val label: String? = null,
|
|
||||||
public val name: String? = null,
|
|
||||||
) : VisionOfHtmlInput() {
|
|
||||||
public var text: String? by mutableProperties.string()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@SerialName("html.checkbox")
|
|
||||||
public class VisionOfCheckbox(
|
|
||||||
public val label: String? = null,
|
|
||||||
public val name: String? = null,
|
|
||||||
) : VisionOfHtmlInput() {
|
|
||||||
public var checked: Boolean? by mutableProperties.boolean()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@SerialName("html.number")
|
|
||||||
public class VisionOfNumberField(
|
|
||||||
public val label: String? = null,
|
|
||||||
public val name: String? = null,
|
|
||||||
) : VisionOfHtmlInput() {
|
|
||||||
public var value: Number? by mutableProperties.number()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@SerialName("html.range")
|
|
||||||
public class VisionOfRangeField(
|
|
||||||
public val min: Double,
|
|
||||||
public val max: Double,
|
|
||||||
public val step: Double = 1.0,
|
|
||||||
public val label: String? = null,
|
|
||||||
public val name: String? = null,
|
|
||||||
) : VisionOfHtmlInput() {
|
|
||||||
public var value: Number? by mutableProperties.number()
|
|
||||||
}
|
|
||||||
|
|
@ -23,10 +23,10 @@ public fun Vision.useProperty(
|
|||||||
callback: (Meta) -> Unit,
|
callback: (Meta) -> Unit,
|
||||||
): Job {
|
): Job {
|
||||||
//Pass initial value.
|
//Pass initial value.
|
||||||
callback(properties.getMeta(propertyName, inherit, includeStyles))
|
callback(properties.get(propertyName, inherit, includeStyles))
|
||||||
return properties.changes.onEach { name ->
|
return properties.changes.onEach { name ->
|
||||||
if (name.startsWith(propertyName)) {
|
if (name.startsWith(propertyName)) {
|
||||||
callback(properties.getMeta(propertyName, inherit, includeStyles))
|
callback(properties.get(propertyName, inherit, includeStyles))
|
||||||
}
|
}
|
||||||
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
|
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
|
||||||
}
|
}
|
||||||
@ -39,9 +39,22 @@ public fun Vision.useProperty(
|
|||||||
callback: (Meta) -> Unit,
|
callback: (Meta) -> Unit,
|
||||||
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)
|
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe changes to the specific property without passing the initial value.
|
||||||
|
*/
|
||||||
|
public fun <V : Vision, T> V.onPropertyChange(
|
||||||
|
property: KProperty1<V, T>,
|
||||||
|
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
|
||||||
|
callback: suspend V.(T) -> Unit,
|
||||||
|
): Job = properties.changes.onEach { name ->
|
||||||
|
if (name.startsWith(property.name.asName())) {
|
||||||
|
callback(property.get(this))
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
public fun <V : Vision, T> V.useProperty(
|
public fun <V : Vision, T> V.useProperty(
|
||||||
property: KProperty1<V, T>,
|
property: KProperty1<V, T>,
|
||||||
scope: CoroutineScope? = manager?.context,
|
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
|
||||||
callback: V.(T) -> Unit,
|
callback: V.(T) -> Unit,
|
||||||
): Job {
|
): Job {
|
||||||
//Pass initial value.
|
//Pass initial value.
|
||||||
@ -50,5 +63,5 @@ public fun <V : Vision, T> V.useProperty(
|
|||||||
if (name.startsWith(property.name.asName())) {
|
if (name.startsWith(property.name.asName())) {
|
||||||
callback(property.get(this@useProperty))
|
callback(property.get(this@useProperty))
|
||||||
}
|
}
|
||||||
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
|
}.launchIn(scope)
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package space.kscience.visionforge.meta
|
package space.kscience.visionforge.meta
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.take
|
||||||
@ -24,7 +23,6 @@ private class TestScheme : Scheme() {
|
|||||||
companion object : SchemeSpec<TestScheme>(::TestScheme)
|
companion object : SchemeSpec<TestScheme>(::TestScheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
internal class VisionPropertyTest {
|
internal class VisionPropertyTest {
|
||||||
|
|
||||||
private val manager = Global.request(VisionManager)
|
private val manager = Global.request(VisionManager)
|
||||||
@ -42,7 +40,7 @@ internal class VisionPropertyTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testPropertyEdit() {
|
fun testPropertyEdit() {
|
||||||
val vision = manager.group()
|
val vision = manager.group()
|
||||||
vision.properties.getMeta("fff.ddd").apply {
|
vision.properties.get("fff.ddd").apply {
|
||||||
value = 2.asValue()
|
value = 2.asValue()
|
||||||
}
|
}
|
||||||
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
|
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
|
||||||
@ -52,7 +50,7 @@ internal class VisionPropertyTest {
|
|||||||
@Test
|
@Test
|
||||||
fun testPropertyUpdate() {
|
fun testPropertyUpdate() {
|
||||||
val vision = manager.group()
|
val vision = manager.group()
|
||||||
vision.properties.getMeta("fff").updateWith(TestScheme) {
|
vision.properties.get("fff").updateWith(TestScheme) {
|
||||||
ddd = 2
|
ddd = 2
|
||||||
}
|
}
|
||||||
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
|
assertEquals(2, vision.properties.getValue("fff.ddd")?.int)
|
||||||
@ -87,7 +85,7 @@ internal class VisionPropertyTest {
|
|||||||
|
|
||||||
child.properties.remove("test")
|
child.properties.remove("test")
|
||||||
|
|
||||||
assertEquals(11, child.properties.getMeta("test", inherit = true).int)
|
assertEquals(11, child.properties.get("test", inherit = true).int)
|
||||||
// assertEquals(11, deferred.await()?.int)
|
// assertEquals(11, deferred.await()?.int)
|
||||||
// assertEquals(2, callCounter)
|
// assertEquals(2, callCounter)
|
||||||
subscription.cancel()
|
subscription.cancel()
|
||||||
|
@ -9,8 +9,8 @@ import kotlinx.serialization.serializerOrNull
|
|||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import org.w3c.dom.HTMLElement
|
import org.w3c.dom.HTMLElement
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.misc.DfType
|
||||||
import space.kscience.dataforge.misc.Named
|
import space.kscience.dataforge.misc.Named
|
||||||
import space.kscience.dataforge.misc.Type
|
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.asName
|
||||||
import space.kscience.dataforge.names.parseAsName
|
import space.kscience.dataforge.names.parseAsName
|
||||||
@ -20,13 +20,13 @@ import kotlin.reflect.cast
|
|||||||
/**
|
/**
|
||||||
* A browser renderer for a [Vision].
|
* A browser renderer for a [Vision].
|
||||||
*/
|
*/
|
||||||
@Type(ElementVisionRenderer.TYPE)
|
@DfType(ElementVisionRenderer.TYPE)
|
||||||
public interface ElementVisionRenderer : Named {
|
public interface ElementVisionRenderer : Named {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
|
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
|
||||||
* can't process a vision. The value of [DEFAULT_RATING] used for default renderer. Specialized renderers could specify
|
* can't process a vision. The value of [DEFAULT_RATING] used for default renderer. Specialized renderers could specify
|
||||||
* higher value in order to "steal" rendering job
|
* higher value to "steal" rendering job
|
||||||
*/
|
*/
|
||||||
public fun rateVision(vision: Vision): Int
|
public fun rateVision(vision: Vision): Int
|
||||||
|
|
||||||
|
@ -67,7 +67,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val changeCollector = VisionChangeBuilder()
|
|
||||||
|
private val rootChangeCollector = VisionChangeBuilder()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Communicate vision property changed from rendering engine to model
|
* Communicate vision property changed from rendering engine to model
|
||||||
@ -75,7 +76,7 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,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)
|
||||||
@ -109,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
|
||||||
@ -125,21 +127,19 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// If change contains root vision replacement, do it
|
// If change contains root vision replacement, do it
|
||||||
if(event is VisionChange) {
|
if (event is VisionChange) {
|
||||||
event.vision?.let { vision ->
|
event.vision?.let { vision ->
|
||||||
renderVision(element, name, vision, outputMeta)
|
renderVision(element, visionName, vision, outputMeta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug { "Got $event 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.receiveEvent(event)
|
||||||
} 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
|
||||||
|
|
||||||
@ -147,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.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))
|
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(name to 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'" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,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")
|
||||||
}
|
}
|
||||||
@ -252,9 +255,13 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
|||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) {
|
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) {
|
||||||
listOf(
|
listOf(
|
||||||
numberVisionRenderer(this),
|
htmlVisionRenderer,
|
||||||
textVisionRenderer(this),
|
inputVisionRenderer,
|
||||||
formVisionRenderer(this)
|
checkboxVisionRenderer,
|
||||||
|
numberVisionRenderer,
|
||||||
|
textVisionRenderer,
|
||||||
|
rangeVisionRenderer,
|
||||||
|
formVisionRenderer
|
||||||
).associateByName()
|
).associateByName()
|
||||||
} else super<AbstractPlugin>.content(target)
|
} else super<AbstractPlugin>.content(target)
|
||||||
|
|
||||||
|
@ -2,66 +2,173 @@ 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 = " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
input {
|
||||||
type = InputType.text
|
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) {
|
vision.useProperty(VisionOfTextField::text) {
|
||||||
value = it ?: ""
|
htmlInputElement.value = it ?: ""
|
||||||
}
|
}
|
||||||
onChangeFunction = {
|
|
||||||
client.notifyPropertyChanged(name, VisionOfTextField::text.name, value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal fun numberVisionRenderer(
|
internal val numberVisionRenderer: ElementVisionRenderer =
|
||||||
client: JsVisionClient,
|
ElementVisionRenderer<VisionOfNumberField> { _, vision, _ ->
|
||||||
): ElementVisionRenderer = ElementVisionRenderer<VisionOfNumberField> { name, vision, _ ->
|
|
||||||
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
|
|
||||||
vision.label?.let {
|
|
||||||
label {
|
|
||||||
htmlFor = fieldName
|
|
||||||
+it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input {
|
input {
|
||||||
type = InputType.text
|
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) {
|
vision.useProperty(VisionOfNumberField::value) {
|
||||||
value = it?.toDouble() ?: 0.0
|
htmlInputElement.valueAsNumber = it?.double ?: 0.0
|
||||||
}
|
}
|
||||||
onChangeFunction = {
|
}
|
||||||
client.notifyPropertyChanged(name, VisionOfNumberField::value.name, value)
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@ -86,17 +193,18 @@ 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.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" }
|
||||||
|
|
||||||
vision.useProperty(VisionOfHtmlForm::values) { values ->
|
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")
|
val inputs = form.getElementsByTagName("input")
|
||||||
values?.valueSequence()?.forEach { (token, value) ->
|
values?.valueSequence()?.forEach { (token, value) ->
|
||||||
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
|
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
|
||||||
@ -106,8 +214,8 @@ internal fun formVisionRenderer(
|
|||||||
form.onsubmit = { event ->
|
form.onsubmit = { event ->
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
val formData = FormData(form).toMeta()
|
val formData = FormData(form).toMeta()
|
||||||
client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData)
|
vision.values = formData
|
||||||
console.info("Sent: ${formData.toMap()}")
|
console.info("Sent: ${formData.toMap()}")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,7 +7,6 @@ import org.jetbrains.kotlinx.jupyter.api.declare
|
|||||||
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
|
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
|
||||||
import space.kscience.visionforge.Vision
|
import space.kscience.visionforge.Vision
|
||||||
import space.kscience.visionforge.VisionManager
|
import space.kscience.visionforge.VisionManager
|
||||||
import space.kscience.visionforge.html.*
|
import space.kscience.visionforge.html.*
|
||||||
@ -17,7 +16,6 @@ import kotlin.random.nextUInt
|
|||||||
/**
|
/**
|
||||||
* A base class for different Jupyter VF integrations
|
* A base class for different Jupyter VF integrations
|
||||||
*/
|
*/
|
||||||
@DFExperimental
|
|
||||||
public abstract class VisionForgeIntegration(
|
public abstract class VisionForgeIntegration(
|
||||||
public val visionManager: VisionManager,
|
public val visionManager: VisionManager,
|
||||||
) : JupyterIntegration(), ContextAware {
|
) : JupyterIntegration(), ContextAware {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package space.kscience.visionforge.jupyter
|
package space.kscience.visionforge.jupyter
|
||||||
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.div
|
||||||
|
import kotlinx.html.p
|
||||||
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
|
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
|
||||||
import space.kscience.gdml.Gdml
|
import space.kscience.gdml.Gdml
|
||||||
import space.kscience.plotly.Plot
|
import space.kscience.plotly.Plot
|
||||||
import space.kscience.plotly.PlotlyPage
|
import space.kscience.plotly.PlotlyPage
|
||||||
import space.kscience.plotly.StaticPlotlyRenderer
|
import space.kscience.plotly.StaticPlotlyRenderer
|
||||||
import space.kscience.tables.*
|
import space.kscience.tables.Table
|
||||||
import space.kscience.visionforge.gdml.toVision
|
import space.kscience.visionforge.gdml.toVision
|
||||||
import space.kscience.visionforge.html.HtmlFragment
|
import space.kscience.visionforge.html.HtmlFragment
|
||||||
import space.kscience.visionforge.html.VisionPage
|
import space.kscience.visionforge.html.VisionPage
|
||||||
@ -21,7 +21,6 @@ import space.kscience.visionforge.tables.toVision
|
|||||||
import space.kscience.visionforge.visionManager
|
import space.kscience.visionforge.visionManager
|
||||||
|
|
||||||
|
|
||||||
@DFExperimental
|
|
||||||
public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) {
|
public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) {
|
||||||
|
|
||||||
override fun Builder.afterLoaded(vf: VisionForge) {
|
override fun Builder.afterLoaded(vf: VisionForge) {
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
}
|
}
|
||||||
|
|
||||||
val plotlyVersion = "0.6.0"
|
val plotlyVersion = "0.6.1"
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
|
@ -33,8 +33,8 @@ public class VisionOfPlotly private constructor(
|
|||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
override val properties: MutableVisionProperties = object : MutableVisionProperties {
|
override val properties: MutableVisionProperties = object : MutableVisionProperties {
|
||||||
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
|
override fun set(name: Name, node: Meta?, notify: Boolean) {
|
||||||
meta.setMeta(name, node)
|
meta[name] = node
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setValue(name: Name, value: Value?, notify: Boolean) {
|
override fun setValue(name: Name, value: Value?, notify: Boolean) {
|
||||||
@ -45,11 +45,11 @@ public class VisionOfPlotly private constructor(
|
|||||||
|
|
||||||
override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor
|
override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor
|
||||||
|
|
||||||
override fun getMeta(
|
override fun get(
|
||||||
name: Name,
|
name: Name,
|
||||||
inherit: Boolean?,
|
inherit: Boolean?,
|
||||||
includeStyles: Boolean?,
|
includeStyles: Boolean?,
|
||||||
): MutableMeta = meta.getMeta(name) ?: MutableMeta()
|
): MutableMeta = meta[name] ?: MutableMeta()
|
||||||
|
|
||||||
override fun getValue(
|
override fun getValue(
|
||||||
name: Name,
|
name: Name,
|
||||||
|
@ -32,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 {
|
||||||
|
@ -39,7 +39,7 @@ public inline fun MutableVisionContainer<Solid>.composite(
|
|||||||
}
|
}
|
||||||
val res = Composite(type, children[0], children[1])
|
val res = Composite(type, children[0], children[1])
|
||||||
|
|
||||||
res.properties.setMeta(Name.EMPTY, group.properties.own)
|
res.properties[Name.EMPTY] = group.properties.own
|
||||||
|
|
||||||
setChild(name, res)
|
setChild(name, res)
|
||||||
return res
|
return res
|
||||||
|
@ -92,7 +92,7 @@ public class Extruded(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun build(): Extruded = Extruded(shape, layers).apply {
|
internal fun build(): Extruded = Extruded(shape, layers).apply {
|
||||||
this.properties.setMeta(Name.EMPTY, this@Builder.properties)
|
this.properties[Name.EMPTY] = this@Builder.properties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ internal fun float32Vector(
|
|||||||
|
|
||||||
override fun setValue(thisRef: Solid, property: KProperty<*>, value: Float32Vector3D?) {
|
override fun setValue(thisRef: Solid, property: KProperty<*>, value: Float32Vector3D?) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
thisRef.properties.setMeta(name, null)
|
thisRef.properties[name] = null
|
||||||
} else {
|
} else {
|
||||||
thisRef.properties[name + X_KEY] = value.x
|
thisRef.properties[name + X_KEY] = value.x
|
||||||
thisRef.properties[name + Y_KEY] = value.y
|
thisRef.properties[name + Y_KEY] = value.y
|
||||||
|
@ -110,12 +110,12 @@ public val Solid.color: ColorAccessor
|
|||||||
get() = ColorAccessor(properties.root(true), MATERIAL_COLOR_KEY)
|
get() = ColorAccessor(properties.root(true), MATERIAL_COLOR_KEY)
|
||||||
|
|
||||||
public var Solid.material: SolidMaterial?
|
public var Solid.material: SolidMaterial?
|
||||||
get() = SolidMaterial.read(properties.getMeta(MATERIAL_KEY))
|
get() = SolidMaterial.read(properties[MATERIAL_KEY])
|
||||||
set(value) = properties.setMeta(MATERIAL_KEY, value?.meta)
|
set(value) = properties.set(MATERIAL_KEY, value?.meta)
|
||||||
|
|
||||||
@VisionBuilder
|
@VisionBuilder
|
||||||
public fun Solid.material(builder: SolidMaterial.() -> Unit) {
|
public fun Solid.material(builder: SolidMaterial.() -> Unit) {
|
||||||
properties.getMeta(MATERIAL_KEY).updateWith(SolidMaterial, builder)
|
properties[MATERIAL_KEY].updateWith(SolidMaterial, builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var Solid.opacity: Number?
|
public var Solid.opacity: Number?
|
||||||
@ -128,5 +128,5 @@ public var Solid.opacity: Number?
|
|||||||
@VisionBuilder
|
@VisionBuilder
|
||||||
public fun Solid.edges(enabled: Boolean = true, block: SolidMaterial.() -> Unit = {}) {
|
public fun Solid.edges(enabled: Boolean = true, block: SolidMaterial.() -> Unit = {}) {
|
||||||
properties[SolidMaterial.EDGES_ENABLED_KEY] = enabled
|
properties[SolidMaterial.EDGES_ENABLED_KEY] = enabled
|
||||||
SolidMaterial.write(properties.getMeta(SolidMaterial.EDGES_MATERIAL_KEY)).apply(block)
|
SolidMaterial.write(properties[SolidMaterial.EDGES_MATERIAL_KEY]).apply(block)
|
||||||
}
|
}
|
@ -162,7 +162,7 @@ internal class SolidReferenceChild(
|
|||||||
override val properties: MutableVisionProperties = object : MutableVisionProperties {
|
override val properties: MutableVisionProperties = object : MutableVisionProperties {
|
||||||
override val descriptor: MetaDescriptor get() = this@SolidReferenceChild.descriptor
|
override val descriptor: MetaDescriptor get() = this@SolidReferenceChild.descriptor
|
||||||
|
|
||||||
override val own: MutableMeta by lazy { owner.properties.getMeta(childToken(childName).asName()) }
|
override val own: MutableMeta by lazy { owner.properties[childToken(childName).asName()] }
|
||||||
|
|
||||||
override fun getValue(
|
override fun getValue(
|
||||||
name: Name,
|
name: Name,
|
||||||
@ -170,8 +170,8 @@ internal class SolidReferenceChild(
|
|||||||
includeStyles: Boolean?,
|
includeStyles: Boolean?,
|
||||||
): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles)
|
): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles)
|
||||||
|
|
||||||
override fun setMeta(name: Name, node: Meta?, notify: Boolean) {
|
override fun set(name: Name, node: Meta?, notify: Boolean) {
|
||||||
own.setMeta(name, node)
|
own[name] = node
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setValue(name: Name, value: Value?, notify: Boolean) {
|
override fun setValue(name: Name, value: Value?, notify: Boolean) {
|
||||||
|
@ -155,7 +155,7 @@ public class Surface(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun build(): Surface = Surface(layers).apply {
|
internal fun build(): Surface = Surface(layers).apply {
|
||||||
properties.setMeta(Name.EMPTY, this@Builder.properties)
|
properties[Name.EMPTY] = this@Builder.properties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,9 +36,9 @@ internal fun Meta.toVector2D(): Float32Vector2D =
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
internal fun MetaProvider.point3D(default: Float = 0f) = Float32Euclidean3DSpace.vector(
|
internal fun MetaProvider.point3D(default: Float = 0f) = Float32Euclidean3DSpace.vector(
|
||||||
getMeta(X_KEY).float ?: default,
|
get(X_KEY).float ?: default,
|
||||||
getMeta(Y_KEY).float ?: default,
|
get(Y_KEY).float ?: default,
|
||||||
getMeta(Z_KEY).float ?: default
|
get(Z_KEY).float ?: default
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ internal fun Solid.updateFrom(other: Solid): Solid {
|
|||||||
scaleX *= other.scaleX
|
scaleX *= other.scaleX
|
||||||
scaleY *= other.scaleY
|
scaleY *= other.scaleY
|
||||||
scaleZ *= other.scaleZ
|
scaleZ *= other.scaleZ
|
||||||
properties.setMeta(Name.EMPTY, other.properties.root())
|
properties[Name.EMPTY] = other.properties.root()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ internal class VisionUpdateTest {
|
|||||||
propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
||||||
propertyChanged("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
propertyChanged("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
|
||||||
}
|
}
|
||||||
targetVision.receiveChange(dif)
|
targetVision.update(dif)
|
||||||
assertTrue { targetVision.children.getChild("top") is SolidGroup }
|
assertTrue { targetVision.children.getChild("top") is SolidGroup }
|
||||||
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
|
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("space.kscience.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
}
|
}
|
||||||
|
|
||||||
val tablesVersion = "0.2.1"
|
val tablesVersion = "0.3.0"
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm()
|
||||||
|
@ -6,6 +6,7 @@ import space.kscience.dataforge.meta.double
|
|||||||
import space.kscience.dataforge.meta.int
|
import space.kscience.dataforge.meta.int
|
||||||
import space.kscience.tables.ColumnHeader
|
import space.kscience.tables.ColumnHeader
|
||||||
import space.kscience.tables.ColumnTable
|
import space.kscience.tables.ColumnTable
|
||||||
|
import space.kscience.tables.fill
|
||||||
import space.kscience.tables.get
|
import space.kscience.tables.get
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
@ -18,7 +19,7 @@ internal class VisionOfTableTest {
|
|||||||
val y by ColumnHeader.typed<Value>()
|
val y by ColumnHeader.typed<Value>()
|
||||||
|
|
||||||
val table = ColumnTable<Value>(100) {
|
val table = ColumnTable<Value>(100) {
|
||||||
x.fill { it.asValue() }
|
fill(x, null) { it.asValue() }
|
||||||
y.values = x.values.map { it?.double?.pow(2)?.asValue() }
|
y.values = x.values.map { it?.double?.pow(2)?.asValue() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package space.kscience.visionforge.solid.three
|
package space.kscience.visionforge.solid.three
|
||||||
|
|
||||||
import space.kscience.dataforge.misc.Type
|
import space.kscience.dataforge.misc.DfType
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.startsWith
|
import space.kscience.dataforge.names.startsWith
|
||||||
import space.kscience.visionforge.Vision
|
import space.kscience.visionforge.Vision
|
||||||
@ -17,7 +17,7 @@ import kotlin.reflect.KClass
|
|||||||
/**
|
/**
|
||||||
* Builder and updater for three.js object
|
* Builder and updater for three.js object
|
||||||
*/
|
*/
|
||||||
@Type(TYPE)
|
@DfType(TYPE)
|
||||||
public interface ThreeFactory<in T : Vision> {
|
public interface ThreeFactory<in T : Vision> {
|
||||||
|
|
||||||
public val type: KClass<in T>
|
public val type: KClass<in T>
|
||||||
|
@ -24,7 +24,7 @@ public object ThreeLineFactory : ThreeFactory<PolyLine> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val material = ThreeMaterials.getLineMaterial(
|
val material = ThreeMaterials.getLineMaterial(
|
||||||
vision.properties.getMeta(SolidMaterial.MATERIAL_KEY),
|
vision.properties[SolidMaterial.MATERIAL_KEY],
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ public object ThreeMaterials {
|
|||||||
private val visionMaterialCache = HashMap<Vision, Material>()
|
private val visionMaterialCache = HashMap<Vision, Material>()
|
||||||
|
|
||||||
internal fun cacheMaterial(vision: Vision): Material = visionMaterialCache.getOrPut(vision) {
|
internal fun cacheMaterial(vision: Vision): Material = visionMaterialCache.getOrPut(vision) {
|
||||||
buildMaterial(vision.properties.getMeta(SolidMaterial.MATERIAL_KEY)).apply {
|
buildMaterial(vision.properties[SolidMaterial.MATERIAL_KEY]).apply {
|
||||||
cached = true
|
cached = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,11 +133,11 @@ public fun Mesh.setMaterial(vision: Vision) {
|
|||||||
} else {
|
} else {
|
||||||
material = vision.parent?.let { parent ->
|
material = vision.parent?.let { parent ->
|
||||||
//TODO cache parent material
|
//TODO cache parent material
|
||||||
ThreeMaterials.buildMaterial(parent.properties.getMeta(SolidMaterial.MATERIAL_KEY))
|
ThreeMaterials.buildMaterial(parent.properties[SolidMaterial.MATERIAL_KEY])
|
||||||
} ?: ThreeMaterials.cacheMaterial(vision)
|
} ?: ThreeMaterials.cacheMaterial(vision)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
material = ThreeMaterials.buildMaterial(vision.properties.getMeta(SolidMaterial.MATERIAL_KEY))
|
material = ThreeMaterials.buildMaterial(vision.properties[SolidMaterial.MATERIAL_KEY])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,18 +153,18 @@ public fun Mesh.updateMaterialProperty(vision: Vision, propertyName: Name) {
|
|||||||
when (propertyName) {
|
when (propertyName) {
|
||||||
SolidMaterial.MATERIAL_COLOR_KEY -> {
|
SolidMaterial.MATERIAL_COLOR_KEY -> {
|
||||||
material.asDynamic().color =
|
material.asDynamic().color =
|
||||||
vision.properties.getMeta(SolidMaterial.MATERIAL_COLOR_KEY).threeColor()
|
vision.properties[SolidMaterial.MATERIAL_COLOR_KEY].threeColor()
|
||||||
?: ThreeMaterials.DEFAULT_COLOR
|
?: ThreeMaterials.DEFAULT_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
SolidMaterial.SPECULAR_COLOR_KEY -> {
|
SolidMaterial.SPECULAR_COLOR_KEY -> {
|
||||||
material.asDynamic().specular =
|
material.asDynamic().specular =
|
||||||
vision.properties.getMeta(SolidMaterial.SPECULAR_COLOR_KEY).threeColor()
|
vision.properties[SolidMaterial.SPECULAR_COLOR_KEY].threeColor()
|
||||||
?: ThreeMaterials.DEFAULT_COLOR
|
?: ThreeMaterials.DEFAULT_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY -> {
|
SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY -> {
|
||||||
material.asDynamic().emissive = vision.properties.getMeta(SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY)
|
material.asDynamic().emissive = vision.properties[SolidMaterial.MATERIAL_EMISSIVE_COLOR_KEY]
|
||||||
.threeColor()
|
.threeColor()
|
||||||
?: ThreeMaterials.BLACK_COLOR
|
?: ThreeMaterials.BLACK_COLOR
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ public fun Mesh.applyEdges(vision: Solid) {
|
|||||||
val edges = children.find { it.name == EDGES_OBJECT_NAME } as? LineSegments
|
val edges = children.find { it.name == EDGES_OBJECT_NAME } as? LineSegments
|
||||||
//inherited edges definition, enabled by default
|
//inherited edges definition, enabled by default
|
||||||
if (vision.properties.getValue(EDGES_ENABLED_KEY, inherit = false)?.boolean != false) {
|
if (vision.properties.getValue(EDGES_ENABLED_KEY, inherit = false)?.boolean != false) {
|
||||||
val material = ThreeMaterials.getLineMaterial(vision.properties.getMeta(EDGES_MATERIAL_KEY), true)
|
val material = ThreeMaterials.getLineMaterial(vision.properties[EDGES_MATERIAL_KEY], true)
|
||||||
if (edges == null) {
|
if (edges == null) {
|
||||||
add(
|
add(
|
||||||
LineSegments(
|
LineSegments(
|
||||||
|
@ -10,12 +10,14 @@ import space.kscience.dataforge.names.*
|
|||||||
import space.kscience.visionforge.*
|
import space.kscience.visionforge.*
|
||||||
import space.kscience.visionforge.solid.*
|
import space.kscience.visionforge.solid.*
|
||||||
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
|
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
|
||||||
import space.kscience.visionforge.solid.three.set
|
|
||||||
import three.core.Object3D
|
import three.core.Object3D
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import three.objects.Group as ThreeGroup
|
import three.objects.Group as ThreeGroup
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A plugin that handles Three Object3D representation of Visions.
|
||||||
|
*/
|
||||||
public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
|
public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
@ -48,6 +50,13 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
|
|||||||
as ThreeFactory<Solid>?
|
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) {
|
public suspend fun buildObject3D(vision: Solid, observe: Boolean = true): Object3D = when (vision) {
|
||||||
is ThreeJsVision -> vision.render(this)
|
is ThreeJsVision -> vision.render(this)
|
||||||
is SolidReference -> ThreeReferenceFactory.build(this, vision, observe)
|
is SolidReference -> ThreeReferenceFactory.build(this, vision, observe)
|
||||||
@ -125,6 +134,16 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
|
|||||||
|
|
||||||
private val canvasCache = HashMap<Element, ThreeCanvas>()
|
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(
|
public fun getOrCreateCanvas(
|
||||||
element: Element,
|
element: Element,
|
||||||
options: Canvas3DOptions,
|
options: Canvas3DOptions,
|
||||||
@ -142,6 +161,19 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
|
|||||||
override fun rateVision(vision: Vision): Int =
|
override fun rateVision(vision: Vision): Int =
|
||||||
if (vision is Solid) ElementVisionRenderer.DEFAULT_RATING else ElementVisionRenderer.ZERO_RATING
|
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(
|
internal fun renderSolid(
|
||||||
element: Element,
|
element: Element,
|
||||||
vision: Solid,
|
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(
|
public fun ThreePlugin.render(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
obj: Solid,
|
obj: Solid,
|
||||||
|
Loading…
Reference in New Issue
Block a user