0.2.0 #71
35
README.md
35
README.md
@ -14,14 +14,6 @@
|
|||||||
* [Features](#features)
|
* [Features](#features)
|
||||||
* [About DataForge](#about-dataforge)
|
* [About DataForge](#about-dataforge)
|
||||||
* [Modules contained in this repository](#modules-contained-in-this-repository)
|
* [Modules contained in this repository](#modules-contained-in-this-repository)
|
||||||
* [visionforge-core](#visionforge-core)
|
|
||||||
* [visionforge-fx](#visionforge-fx)
|
|
||||||
* [visionforge-gdml](#visionforge-gdml)
|
|
||||||
* [visionforge-markdown](#visionforge-markdown)
|
|
||||||
* [visionforge-plotly](#visionforge-plotly)
|
|
||||||
* [visionforge-server](#visionforge-server)
|
|
||||||
* [visionforge-solid](#visionforge-solid)
|
|
||||||
* [visionforge-threejs](#visionforge-threejs)
|
|
||||||
* [Visualization for External Systems](#visualization-for-external-systems)
|
* [Visualization for External Systems](#visualization-for-external-systems)
|
||||||
* [Demonstrations](#demonstrations)
|
* [Demonstrations](#demonstrations)
|
||||||
* [Simple Example - Solid Showcase](#simple-example---solid-showcase)
|
* [Simple Example - Solid Showcase](#simple-example---solid-showcase)
|
||||||
@ -69,32 +61,7 @@ To learn more about DataForge, please consult the following URLs:
|
|||||||
|
|
||||||
## Modules contained in this repository
|
## Modules contained in this repository
|
||||||
|
|
||||||
### visionforge-core
|
$modules
|
||||||
|
|
||||||
Contains a general hierarchy of classes and interfaces useful for visualization.
|
|
||||||
This module is not specific to 3D-visualization.
|
|
||||||
|
|
||||||
The `visionforge-core` module also includes configuration editors for JS (in `jsMain`) and JVM (in `jvmMain`).
|
|
||||||
|
|
||||||
**Class diagram:**
|
|
||||||
|
|
||||||
![](docs/images/class-diag-core.png)
|
|
||||||
|
|
||||||
### visionforge-fx
|
|
||||||
|
|
||||||
### visionforge-gdml
|
|
||||||
|
|
||||||
GDML bindings for 3D visualization (to be moved to gdml project).
|
|
||||||
|
|
||||||
### visionforge-markdown
|
|
||||||
|
|
||||||
### visionforge-plotly
|
|
||||||
|
|
||||||
### visionforge-server
|
|
||||||
|
|
||||||
### visionforge-solid
|
|
||||||
|
|
||||||
Includes common classes and serializers for 3D visualization, as well as Three.js and JavaFX implementations.
|
|
||||||
|
|
||||||
**Class diagram:**
|
**Class diagram:**
|
||||||
|
|
||||||
|
@ -1,26 +1,23 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.project")
|
id("ru.mipt.npm.gradle.project")
|
||||||
|
id("org.jetbrains.kotlinx.kover") version "0.5.0-RC"
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataforgeVersion by extra("0.5.2")
|
val dataforgeVersion by extra("0.5.2")
|
||||||
val fxVersion by extra("11")
|
val fxVersion by extra("11")
|
||||||
|
|
||||||
allprojects {
|
subprojects {
|
||||||
|
if (name.startsWith("visionforge")) apply<MavenPublishPlugin>()
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
|
mavenCentral()
|
||||||
maven("https://maven.jzy3d.org/releases")
|
maven("https://maven.jzy3d.org/releases")
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.2.0"
|
version = "0.2.0-dev-99"
|
||||||
}
|
|
||||||
|
|
||||||
subprojects {
|
|
||||||
if (name.startsWith("visionforge")) {
|
|
||||||
plugins.apply("maven-publish")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ksciencePublish {
|
ksciencePublish {
|
||||||
@ -30,10 +27,10 @@ ksciencePublish {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiValidation {
|
apiValidation {
|
||||||
validationDisabled = true
|
|
||||||
ignoredPackages.add("info.laht.threekt")
|
ignoredPackages.add("info.laht.threekt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
||||||
|
|
||||||
//workaround for https://youtrack.jetbrains.com/issue/KT-48273
|
//workaround for https://youtrack.jetbrains.com/issue/KT-48273
|
||||||
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
|
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
|
||||||
|
@ -16,6 +16,7 @@ kotlin {
|
|||||||
jvm {
|
jvm {
|
||||||
withJava()
|
withJava()
|
||||||
}
|
}
|
||||||
|
|
||||||
js {
|
js {
|
||||||
useCommonJs()
|
useCommonJs()
|
||||||
browser {
|
browser {
|
||||||
@ -24,6 +25,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -47,8 +47,9 @@ kotlin {
|
|||||||
implementation(projects.visionforgeGdml)
|
implementation(projects.visionforgeGdml)
|
||||||
implementation(projects.visionforgePlotly)
|
implementation(projects.visionforgePlotly)
|
||||||
implementation(projects.visionforgeMarkdown)
|
implementation(projects.visionforgeMarkdown)
|
||||||
|
implementation(projects.visionforgeTables)
|
||||||
implementation(projects.cernRootLoader)
|
implementation(projects.cernRootLoader)
|
||||||
implementation(projects.jupyter.jupyterBase)
|
implementation(projects.jupyter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.ui.ring)
|
implementation(projects.ui.ring)
|
||||||
implementation(projects.visionforgeThreejs)
|
implementation(projects.visionforgeThreejs)
|
||||||
|
compileOnly(npm("webpack-bundle-analyzer","4.5.0"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +68,9 @@ kotlin {
|
|||||||
implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
all {
|
||||||
|
languageSettings.optIn("space.kscience.dataforge.misc.DFExperimental")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,10 +3,12 @@ import space.kscience.visionforge.markup.MarkupPlugin
|
|||||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
import space.kscience.visionforge.plotly.PlotlyPlugin
|
||||||
import space.kscience.visionforge.ring.ThreeWithControlsPlugin
|
import space.kscience.visionforge.ring.ThreeWithControlsPlugin
|
||||||
import space.kscience.visionforge.runVisionClient
|
import space.kscience.visionforge.runVisionClient
|
||||||
|
import space.kscience.visionforge.tables.TableVisionJsPlugin
|
||||||
|
|
||||||
@DFExperimental
|
@DFExperimental
|
||||||
fun main() = runVisionClient {
|
fun main() = runVisionClient {
|
||||||
plugin(ThreeWithControlsPlugin)
|
plugin(ThreeWithControlsPlugin)
|
||||||
plugin(PlotlyPlugin)
|
plugin(PlotlyPlugin)
|
||||||
plugin(MarkupPlugin)
|
plugin(MarkupPlugin)
|
||||||
|
plugin(TableVisionJsPlugin)
|
||||||
}
|
}
|
@ -1,40 +0,0 @@
|
|||||||
/* Remove default bullets */
|
|
||||||
ul, .tree {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style the caret/arrow */
|
|
||||||
.tree-caret {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none; /* Prevent text selection */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Create the caret/arrow with a unicode, and style it */
|
|
||||||
.tree-caret::before {
|
|
||||||
content: "\25B6";
|
|
||||||
color: black;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
|
|
||||||
.tree-caret-down::before {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, .tree {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
i, .tree-caret{
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rotate {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-label-inactive {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Playground</title>
|
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
|
|
||||||
<link rel="stylesheet" href="css/common.css">
|
|
||||||
<script type="text/javascript" src="playground.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="application">
|
|
||||||
<div class="container">
|
|
||||||
<h1>Playground</h1>
|
|
||||||
</div>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
181
demo/playground/src/jvmMain/kotlin/allThingsDemo.kt
Normal file
181
demo/playground/src/jvmMain/kotlin/allThingsDemo.kt
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package space.kscience.visionforge.examples
|
||||||
|
|
||||||
|
import kotlinx.html.h1
|
||||||
|
import kotlinx.html.h2
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.values.ValueType
|
||||||
|
import space.kscience.plotly.layout
|
||||||
|
import space.kscience.plotly.models.ScatterMode
|
||||||
|
import space.kscience.plotly.models.TextPosition
|
||||||
|
import space.kscience.plotly.scatter
|
||||||
|
import space.kscience.tables.ColumnHeader
|
||||||
|
import space.kscience.visionforge.html.ResourceLocation
|
||||||
|
import space.kscience.visionforge.markup.markdown
|
||||||
|
import space.kscience.visionforge.plotly.PlotlyPlugin
|
||||||
|
import space.kscience.visionforge.plotly.plotly
|
||||||
|
import space.kscience.visionforge.solid.*
|
||||||
|
import space.kscience.visionforge.tables.TableVisionPlugin
|
||||||
|
import space.kscience.visionforge.tables.columnTable
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val context = Context {
|
||||||
|
plugin(Solids)
|
||||||
|
plugin(PlotlyPlugin)
|
||||||
|
plugin(TableVisionPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.makeVisionFile(
|
||||||
|
Paths.get("VisionForgeDemo.html"),
|
||||||
|
resourceLocation = ResourceLocation.EMBED
|
||||||
|
) {
|
||||||
|
markdown {
|
||||||
|
//language=markdown
|
||||||
|
"""
|
||||||
|
# VisionForge
|
||||||
|
|
||||||
|
This is a demo for current VisionForge features. This text is written in [MarkDown](https://github.com/JetBrains/markdown)
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 { +"3D visualization with Three-js" }
|
||||||
|
vision("3D") {
|
||||||
|
solid {
|
||||||
|
box(100, 100, 100, name = "aBox")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 { +"Interactive plots with Plotly" }
|
||||||
|
vision("plot") {
|
||||||
|
plotly {
|
||||||
|
scatter {
|
||||||
|
x(1, 2, 3, 4)
|
||||||
|
y(10, 15, 13, 17)
|
||||||
|
mode = ScatterMode.markers
|
||||||
|
name = "Team A"
|
||||||
|
text("A-1", "A-2", "A-3", "A-4", "A-5")
|
||||||
|
textposition = TextPosition.`top center`
|
||||||
|
textfont {
|
||||||
|
family = "Raleway, sans-serif"
|
||||||
|
}
|
||||||
|
marker { size = 12 }
|
||||||
|
}
|
||||||
|
|
||||||
|
scatter {
|
||||||
|
x(2, 3, 4, 5)
|
||||||
|
y(10, 15, 13, 17)
|
||||||
|
mode = ScatterMode.lines
|
||||||
|
name = "Team B"
|
||||||
|
text("B-a", "B-b", "B-c", "B-d", "B-e")
|
||||||
|
textposition = TextPosition.`bottom center`
|
||||||
|
textfont {
|
||||||
|
family = "Times New Roman"
|
||||||
|
}
|
||||||
|
marker { size = 12 }
|
||||||
|
}
|
||||||
|
|
||||||
|
layout {
|
||||||
|
title = "Data Labels Hover"
|
||||||
|
xaxis {
|
||||||
|
range(0.75..5.25)
|
||||||
|
}
|
||||||
|
legend {
|
||||||
|
y = 0.5
|
||||||
|
font {
|
||||||
|
family = "Arial, sans-serif"
|
||||||
|
size = 20
|
||||||
|
color("grey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h2 { +"Interactive tables with Tabulator" }
|
||||||
|
vision("table") {
|
||||||
|
val x by ColumnHeader.value(ValueType.NUMBER)
|
||||||
|
val y by ColumnHeader.value(ValueType.NUMBER)
|
||||||
|
columnTable(
|
||||||
|
x to listOf(2, 3, 4, 5),
|
||||||
|
y to listOf(10, 15, 13, 17)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
markdown {
|
||||||
|
//language=markdown
|
||||||
|
"""
|
||||||
|
## The code for everything above
|
||||||
|
```kotlin
|
||||||
|
markdown {
|
||||||
|
//language=markdown
|
||||||
|
""${'"'}
|
||||||
|
# VisionForge
|
||||||
|
|
||||||
|
This is a demo for current VisionForge features. This text is written in [MarkDown](https://github.com/JetBrains/markdown)
|
||||||
|
""${'"'}.trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 { +"3D visualization with Three-js" }
|
||||||
|
vision("3D") {
|
||||||
|
solid {
|
||||||
|
box(100, 100, 100, name = "aBox")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 { +"Interactive plots with Plotly" }
|
||||||
|
vision("plot") {
|
||||||
|
plotly {
|
||||||
|
scatter {
|
||||||
|
x(1, 2, 3, 4)
|
||||||
|
y(10, 15, 13, 17)
|
||||||
|
mode = ScatterMode.markers
|
||||||
|
name = "Team A"
|
||||||
|
text("A-1", "A-2", "A-3", "A-4", "A-5")
|
||||||
|
textposition = TextPosition.`top center`
|
||||||
|
textfont {
|
||||||
|
family = "Raleway, sans-serif"
|
||||||
|
}
|
||||||
|
marker { size = 12 }
|
||||||
|
}
|
||||||
|
|
||||||
|
scatter {
|
||||||
|
x(2, 3, 4, 5)
|
||||||
|
y(10, 15, 13, 17)
|
||||||
|
mode = ScatterMode.lines
|
||||||
|
name = "Team B"
|
||||||
|
text("B-a", "B-b", "B-c", "B-d", "B-e")
|
||||||
|
textposition = TextPosition.`bottom center`
|
||||||
|
textfont {
|
||||||
|
family = "Times New Roman"
|
||||||
|
}
|
||||||
|
marker { size = 12 }
|
||||||
|
}
|
||||||
|
|
||||||
|
layout {
|
||||||
|
title = "Data Labels Hover"
|
||||||
|
xaxis {
|
||||||
|
range(0.75..5.25)
|
||||||
|
}
|
||||||
|
legend {
|
||||||
|
y = 0.5
|
||||||
|
font {
|
||||||
|
family = "Arial, sans-serif"
|
||||||
|
size = 20
|
||||||
|
color("grey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h2 { +"Interactive tables with Tabulator" }
|
||||||
|
vision("table") {
|
||||||
|
val x by ColumnHeader.value(ValueType.NUMBER)
|
||||||
|
val y by ColumnHeader.value(ValueType.NUMBER)
|
||||||
|
columnTable(
|
||||||
|
x to listOf(2, 3, 4, 5),
|
||||||
|
y to listOf(10, 15, 13, 17)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,91 +0,0 @@
|
|||||||
package space.kscience.visionforge.examples
|
|
||||||
|
|
||||||
import kotlinx.html.div
|
|
||||||
import kotlinx.html.h1
|
|
||||||
import space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.plotly.layout
|
|
||||||
import space.kscience.plotly.models.ScatterMode
|
|
||||||
import space.kscience.plotly.models.TextPosition
|
|
||||||
import space.kscience.plotly.scatter
|
|
||||||
import space.kscience.visionforge.html.ResourceLocation
|
|
||||||
import space.kscience.visionforge.markup.markdown
|
|
||||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
|
||||||
import space.kscience.visionforge.plotly.plotly
|
|
||||||
import space.kscience.visionforge.solid.*
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
val context = Context {
|
|
||||||
plugin(Solids)
|
|
||||||
plugin(PlotlyPlugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.makeVisionFile(resourceLocation = ResourceLocation.SYSTEM) {
|
|
||||||
markdown {
|
|
||||||
//language=markdown
|
|
||||||
"""
|
|
||||||
# Section
|
|
||||||
|
|
||||||
**TBD**
|
|
||||||
|
|
||||||
## Subsection
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
h1 { +"Canvas" }
|
|
||||||
vision("canvas") {
|
|
||||||
solid {
|
|
||||||
box(100, 100, 100)
|
|
||||||
material {
|
|
||||||
emissiveColor("red")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vision("plot") {
|
|
||||||
plotly {
|
|
||||||
scatter {
|
|
||||||
x(1, 2, 3, 4)
|
|
||||||
y(10, 15, 13, 17)
|
|
||||||
mode = ScatterMode.markers
|
|
||||||
name = "Team A"
|
|
||||||
text("A-1", "A-2", "A-3", "A-4", "A-5")
|
|
||||||
textposition = TextPosition.`top center`
|
|
||||||
textfont {
|
|
||||||
family = "Raleway, sans-serif"
|
|
||||||
}
|
|
||||||
marker { size = 12 }
|
|
||||||
}
|
|
||||||
|
|
||||||
scatter {
|
|
||||||
x(2, 3, 4, 5)
|
|
||||||
y(10, 15, 13, 17)
|
|
||||||
mode = ScatterMode.lines
|
|
||||||
name = "Team B"
|
|
||||||
text("B-a", "B-b", "B-c", "B-d", "B-e")
|
|
||||||
textposition = TextPosition.`bottom center`
|
|
||||||
textfont {
|
|
||||||
family = "Times New Roman"
|
|
||||||
}
|
|
||||||
marker { size = 12 }
|
|
||||||
}
|
|
||||||
|
|
||||||
layout {
|
|
||||||
title = "Data Labels Hover"
|
|
||||||
xaxis {
|
|
||||||
range(0.75..5.25)
|
|
||||||
}
|
|
||||||
legend {
|
|
||||||
y = 0.5
|
|
||||||
font {
|
|
||||||
family = "Arial, sans-serif"
|
|
||||||
size = 20
|
|
||||||
color("grey")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,10 +24,13 @@ public fun Context.makeVisionFile(
|
|||||||
title: String = "VisionForge page",
|
title: String = "VisionForge page",
|
||||||
resourceLocation: ResourceLocation = ResourceLocation.SYSTEM,
|
resourceLocation: ResourceLocation = ResourceLocation.SYSTEM,
|
||||||
show: Boolean = true,
|
show: Boolean = true,
|
||||||
content: VisionTagConsumer<*>.() -> Unit
|
content: VisionTagConsumer<*>.() -> Unit,
|
||||||
): Unit {
|
): Unit {
|
||||||
val actualPath = visionManager.page(title, content = content).makeFile(path) { actualPath ->
|
val actualPath = visionManager.page(title, content = content).makeFile(path) { actualPath ->
|
||||||
mapOf("playground" to scriptHeader("js/visionforge-playground.js", resourceLocation, actualPath))
|
mapOf(
|
||||||
|
"playground" to scriptHeader("js/visionforge-playground.js", resourceLocation, actualPath),
|
||||||
|
//"tables" to tabulatorCssHader
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI())
|
if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI())
|
||||||
}
|
}
|
||||||
|
28
demo/playground/src/jvmMain/kotlin/tables.kt
Normal file
28
demo/playground/src/jvmMain/kotlin/tables.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package space.kscience.visionforge.examples
|
||||||
|
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.values.ValueType
|
||||||
|
import space.kscience.tables.ColumnHeader
|
||||||
|
import space.kscience.tables.valueRow
|
||||||
|
import space.kscience.visionforge.html.ResourceLocation
|
||||||
|
import space.kscience.visionforge.tables.TableVisionPlugin
|
||||||
|
import space.kscience.visionforge.tables.table
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val context = Context {
|
||||||
|
plugin(TableVisionPlugin)
|
||||||
|
}
|
||||||
|
val x by ColumnHeader.value(ValueType.NUMBER)
|
||||||
|
val y by ColumnHeader.value(ValueType.NUMBER)
|
||||||
|
|
||||||
|
context.makeVisionFile(resourceLocation = ResourceLocation.SYSTEM) {
|
||||||
|
vision {
|
||||||
|
table(x, y) {
|
||||||
|
repeat(100) {
|
||||||
|
valueRow(x to it, y to it.toDouble().pow(2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
demo/playground/webpack.config.d/01.ring.js
vendored
19
demo/playground/webpack.config.d/01.ring.js
vendored
@ -1,3 +1,22 @@
|
|||||||
const ringConfig = require('@jetbrains/ring-ui/webpack.config').config;
|
const ringConfig = require('@jetbrains/ring-ui/webpack.config').config;
|
||||||
|
|
||||||
config.module.rules.push(...ringConfig.module.rules)
|
config.module.rules.push(...ringConfig.module.rules)
|
||||||
|
|
||||||
|
config.module.rules.push(
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
exclude: [
|
||||||
|
'D:\\Work\\Projects\\visionforge\\build\\js\\node_modules\\@jetbrains\\ring-ui'
|
||||||
|
],
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'style-loader',
|
||||||
|
options: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
10
demo/playground/webpack.config.d/02.bundle.js
vendored
Normal file
10
demo/playground/webpack.config.d/02.bundle.js
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: "static",
|
||||||
|
reportFilename: "bundle-report.html"
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
26
docs/templates/ARTIFACT-TEMPLATE.md
vendored
Normal file
26
docs/templates/ARTIFACT-TEMPLATE.md
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
## Artifact:
|
||||||
|
|
||||||
|
The Maven coordinates of this project are `${group}:${name}:${version}`.
|
||||||
|
|
||||||
|
**Gradle Groovy:**
|
||||||
|
```gradle
|
||||||
|
repositories {
|
||||||
|
maven { url 'https://repo.kotlin.link' }
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation '${group}:${name}:${version}'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Gradle Kotlin DSL:**
|
||||||
|
```kotlin
|
||||||
|
repositories {
|
||||||
|
maven("https://repo.kotlin.link")
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("${group}:${name}:${version}")
|
||||||
|
}
|
||||||
|
```
|
138
docs/templates/README-TEMPLATE.md
vendored
Normal file
138
docs/templates/README-TEMPLATE.md
vendored
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
|
||||||
|
[![DOI](https://zenodo.org/badge/174502624.svg)](https://zenodo.org/badge/latestdoi/174502624)
|
||||||
|
|
||||||
|
![Gradle build](https://github.com/mipt-npm/visionforge/workflows/Gradle%20build/badge.svg)
|
||||||
|
|
||||||
|
[![Slack](https://img.shields.io/badge/slack-channel-green?logo=slack)](https://kotlinlang.slack.com/archives/CEXV2QWNM)
|
||||||
|
|
||||||
|
# DataForge Visualization Platform
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
* [Introduction](#introduction)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
* [Features](#features)
|
||||||
|
* [About DataForge](#about-dataforge)
|
||||||
|
* [Modules contained in this repository](#modules-contained-in-this-repository)
|
||||||
|
* [Visualization for External Systems](#visualization-for-external-systems)
|
||||||
|
* [Demonstrations](#demonstrations)
|
||||||
|
* [Simple Example - Solid Showcase](#simple-example---solid-showcase)
|
||||||
|
* [Full-Stack Application Example - Muon Monitor](#full-stack-application-example---muon-monitor-visualization)
|
||||||
|
* [GDML Example](#gdml-example)
|
||||||
|
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This repository contains a [DataForge](#about-dataforge)\-based framework
|
||||||
|
used for visualization in various scientific applications.
|
||||||
|
|
||||||
|
The main framework's use case for now is 3D visualization for particle physics experiments.
|
||||||
|
Other applications including 2D plots are planned for the future.
|
||||||
|
|
||||||
|
The project is developed as a [Kotlin multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html)
|
||||||
|
application, currently targeting browser JavaScript and JVM.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
JVM backend requires JDK 11 or later
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The main framework's features for now include:
|
||||||
|
- 3D visualization of complex experimental set-ups
|
||||||
|
- Event display such as particle tracks, etc.
|
||||||
|
- Scales up to few hundred thousands of elements
|
||||||
|
- Camera move, rotate, zoom-in and zoom-out
|
||||||
|
- Scene graph as an object tree with property editor
|
||||||
|
- Settings export and import
|
||||||
|
- Multiple platform support
|
||||||
|
|
||||||
|
## About DataForge
|
||||||
|
|
||||||
|
DataForge is a software framework for automated scientific data processing. DataForge Visualization
|
||||||
|
Platform uses some of the concepts and modules of DataForge, including: `Meta`, `Configuration`, `Context`,
|
||||||
|
`Provider`, and some others.
|
||||||
|
|
||||||
|
To learn more about DataForge, please consult the following URLs:
|
||||||
|
* [Kotlin multiplatform implementation of DataForge](https://github.com/mipt-npm/dataforge-core)
|
||||||
|
* [DataForge documentation](http://npm.mipt.ru/dataforge/)
|
||||||
|
* [Original implementation of DataForge](https://bitbucket.org/Altavir/dataforge/src/default/)
|
||||||
|
|
||||||
|
|
||||||
|
## Modules contained in this repository
|
||||||
|
|
||||||
|
$modules
|
||||||
|
|
||||||
|
**Class diagram:**
|
||||||
|
|
||||||
|
![](docs/images/class-diag-solid.png)
|
||||||
|
|
||||||
|
##### Prototypes
|
||||||
|
|
||||||
|
One of the important features of the framework is support for 3D object prototypes (sometimes
|
||||||
|
also referred to as templates). The idea is that prototype geometry can be rendered once and reused
|
||||||
|
for multiple objects. This helps to significantly decrease memory usage.
|
||||||
|
|
||||||
|
The `prototypes` property tree is defined in `SolidGroup` class via `PrototypeHolder` interface, and
|
||||||
|
`SolidReference` class helps to reuse a template object.
|
||||||
|
|
||||||
|
##### Styles
|
||||||
|
|
||||||
|
`VisionGroup` has a `styleSheet` property that can optionally define styles at the Group's
|
||||||
|
level. Styles are applied to child (descendant) objects using `Vision.styles: List<String>` property.
|
||||||
|
|
||||||
|
### visionforge-threejs
|
||||||
|
|
||||||
|
## Visualization for External Systems
|
||||||
|
|
||||||
|
The `visionforge` framework can be used to visualize geometry and events from external,
|
||||||
|
non-Kotlin based systems, such as ROOT. This will require a plugin to convert data model
|
||||||
|
of the external system to that of `visionforge`. Performing such integration is a work
|
||||||
|
currently in progress.
|
||||||
|
|
||||||
|
|
||||||
|
## Demonstrations
|
||||||
|
|
||||||
|
The `demo` module contains several example projects (demonstrations) of using the `visionforge` framework.
|
||||||
|
They are briefly described in this section, for more details please consult the corresponding per-project
|
||||||
|
README file.
|
||||||
|
|
||||||
|
### Simple Example - Solid Showcase
|
||||||
|
|
||||||
|
Contains a simple demonstration with a grid including a few shapes that you can rotate, move camera, and so on.
|
||||||
|
Some shapes will also periodically change their color and visibility.
|
||||||
|
|
||||||
|
[More details](demo/solid-showcase/README.md)
|
||||||
|
|
||||||
|
**Example view:**
|
||||||
|
|
||||||
|
![](docs/images/solid-showcase.png)
|
||||||
|
|
||||||
|
|
||||||
|
### Full-Stack Application Example - Muon Monitor Visualization
|
||||||
|
|
||||||
|
A full-stack application example, showing the
|
||||||
|
[Muon Monitor](http://npm.mipt.ru/en/projects/physics#mounMonitor) experiment set-up.
|
||||||
|
|
||||||
|
[More details](demo/muon-monitor/README.md)
|
||||||
|
|
||||||
|
**Example view:**
|
||||||
|
|
||||||
|
![](docs/images/muon-monitor.png)
|
||||||
|
|
||||||
|
|
||||||
|
### GDML Example
|
||||||
|
|
||||||
|
Visualization example for geometry defined as GDML file.
|
||||||
|
|
||||||
|
[More details](demo/gdml/README.md)
|
||||||
|
|
||||||
|
##### Example view:
|
||||||
|
|
||||||
|
![](docs/images/gdml-demo.png)
|
||||||
|
|
||||||
|
|
||||||
|
## Thanks and references
|
||||||
|
The original three.js bindings were made by [Lars Ivar Hatledal](https://github.com/markaren), but the project is discontinued right now.
|
||||||
|
|
||||||
|
All other libraries are explicitly shown as dependencies. We would like to express specific thanks to JetBrains Kotlin-JS team for consulting us during the work.
|
@ -33,7 +33,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.visionforgeSolid)
|
implementation(projects.visionforgeSolid)
|
||||||
implementation(projects.jupyter.jupyterBase)
|
implementation(projects.jupyter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmMain {
|
jvmMain {
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
rootProject.name = "visionforge"
|
||||||
|
|
||||||
|
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
|
enableFeaturePreview("VERSION_CATALOGS")
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
|
|
||||||
val toolsVersion: String by extra
|
val toolsVersion: String by extra
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
//mavenLocal()
|
mavenLocal()
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
@ -17,16 +22,12 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "visionforge"
|
|
||||||
|
|
||||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
|
||||||
enableFeaturePreview("VERSION_CATALOGS")
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
|
|
||||||
val toolsVersion: String by extra
|
val toolsVersion: String by extra
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
@ -53,6 +54,7 @@ include(
|
|||||||
":cern-root-loader",
|
":cern-root-loader",
|
||||||
":visionforge-server",
|
":visionforge-server",
|
||||||
":visionforge-plotly",
|
":visionforge-plotly",
|
||||||
|
":visionforge-tables",
|
||||||
":visionforge-markdown",
|
":visionforge-markdown",
|
||||||
":demo:solid-showcase",
|
":demo:solid-showcase",
|
||||||
":demo:gdml",
|
":demo:gdml",
|
||||||
@ -61,6 +63,6 @@ include(
|
|||||||
":demo:playground",
|
":demo:playground",
|
||||||
":demo:plotly-fx",
|
":demo:plotly-fx",
|
||||||
":demo:js-playground",
|
":demo:js-playground",
|
||||||
":jupyter:jupyter-base",
|
":jupyter",
|
||||||
":jupyter:visionforge-jupyter-gdml"
|
":jupyter:visionforge-jupyter-gdml"
|
||||||
)
|
)
|
||||||
|
@ -82,8 +82,10 @@ public val ThreeCanvasWithControls: FC<ThreeCanvasWithControlsProps> = fc("Three
|
|||||||
|
|
||||||
useEffect {
|
useEffect {
|
||||||
props.context.launch {
|
props.context.launch {
|
||||||
solid = props.builderOfSolid.await().also {
|
solid = props.builderOfSolid.await()
|
||||||
it?.setAsRoot(props.context.visionManager)
|
//ensure that the solid is properly rooted
|
||||||
|
if(solid?.parent == null){
|
||||||
|
solid?.setAsRoot(props.context.visionManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,6 @@ plugins {
|
|||||||
|
|
||||||
val dataforgeVersion: String by rootProject.extra
|
val dataforgeVersion: String by rootProject.extra
|
||||||
|
|
||||||
kscience{
|
|
||||||
useSerialization{
|
|
||||||
json()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
@ -26,3 +20,13 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kscience{
|
||||||
|
useSerialization{
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readme{
|
||||||
|
maturity = ru.mipt.npm.gradle.Maturity.DEVELOPMENT
|
||||||
|
}
|
@ -163,6 +163,6 @@ internal class RootVisionGroup(override val manager: VisionManager) : VisionGrou
|
|||||||
* Designate this [VisionGroup] as a root and assign a [VisionManager] as its parent
|
* Designate this [VisionGroup] as a root and assign a [VisionManager] as its parent
|
||||||
*/
|
*/
|
||||||
public fun Vision.setAsRoot(manager: VisionManager) {
|
public fun Vision.setAsRoot(manager: VisionManager) {
|
||||||
if (parent != null) error("This Vision already has a parent. It could not be set as root")
|
if (parent != null) error("Vision $this already has a parent. It could not be set as root")
|
||||||
parent = RootVisionGroup(manager)
|
parent = RootVisionGroup(manager)
|
||||||
}
|
}
|
@ -78,6 +78,10 @@ public abstract class VisionTagConsumer<R>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a vision in this HTML.
|
||||||
|
* TODO replace by multi-receiver
|
||||||
|
*/
|
||||||
@OptIn(DFExperimental::class)
|
@OptIn(DFExperimental::class)
|
||||||
public inline fun <T> TagConsumer<T>.vision(
|
public inline fun <T> TagConsumer<T>.vision(
|
||||||
name: Name,
|
name: Name,
|
||||||
@ -122,6 +126,8 @@ public abstract class VisionTagConsumer<R>(
|
|||||||
public const val OUTPUT_FETCH_ATTRIBUTE: String = "data-output-fetch"
|
public const val OUTPUT_FETCH_ATTRIBUTE: String = "data-output-fetch"
|
||||||
public const val OUTPUT_CONNECT_ATTRIBUTE: String = "data-output-connect"
|
public const val OUTPUT_CONNECT_ATTRIBUTE: String = "data-output-connect"
|
||||||
|
|
||||||
|
public const val OUTPUT_RENDERED: String = "data-output-rendered"
|
||||||
|
|
||||||
public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name"
|
public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name"
|
||||||
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
|
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
|
||||||
public const val DEFAULT_ENDPOINT: String = "."
|
public const val DEFAULT_ENDPOINT: String = "."
|
||||||
|
@ -19,6 +19,7 @@ import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNEC
|
|||||||
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
|
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
|
||||||
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_FETCH_ATTRIBUTE
|
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_FETCH_ATTRIBUTE
|
||||||
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE
|
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE
|
||||||
|
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_RENDERED
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@ -64,7 +65,8 @@ public class VisionClient : AbstractPlugin() {
|
|||||||
|
|
||||||
private fun renderVision(name: String, element: Element, vision: Vision?, outputMeta: Meta) {
|
private fun renderVision(name: String, element: Element, vision: Vision?, outputMeta: Meta) {
|
||||||
if (vision != null) {
|
if (vision != null) {
|
||||||
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
|
val renderer = findRendererFor(vision)
|
||||||
|
?: error("Could not find renderer for ${visionManager.encodeToString(vision)}")
|
||||||
renderer.render(element, vision, outputMeta)
|
renderer.render(element, vision, outputMeta)
|
||||||
|
|
||||||
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
|
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
|
||||||
@ -138,10 +140,15 @@ public class VisionClient : AbstractPlugin() {
|
|||||||
* Fetch from server and render a vision, described in a given with [VisionTagConsumer.OUTPUT_CLASS] class.
|
* Fetch from server and render a vision, described in a given with [VisionTagConsumer.OUTPUT_CLASS] class.
|
||||||
*/
|
*/
|
||||||
public fun renderVisionIn(element: Element) {
|
public fun renderVisionIn(element: Element) {
|
||||||
val name = resolveName(element) ?: error("The element is not a vision output")
|
|
||||||
logger.info { "Found DF output with name $name" }
|
|
||||||
if (!element.classList.contains(VisionTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element")
|
if (!element.classList.contains(VisionTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element")
|
||||||
|
val name = resolveName(element) ?: error("The element is not a vision output")
|
||||||
|
|
||||||
|
if (element.attributes[OUTPUT_RENDERED]?.value == "true") {
|
||||||
|
logger.info { "VF output in element $element is already rendered" }
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
logger.info { "Rendering VF output with name $name" }
|
||||||
|
}
|
||||||
|
|
||||||
val outputMeta = element.getEmbeddedData(VisionTagConsumer.OUTPUT_META_CLASS)?.let {
|
val outputMeta = element.getEmbeddedData(VisionTagConsumer.OUTPUT_META_CLASS)?.let {
|
||||||
VisionManager.defaultJson.decodeFromString(MetaSerializer, it)
|
VisionManager.defaultJson.decodeFromString(MetaSerializer, it)
|
||||||
@ -186,6 +193,7 @@ public class VisionClient : AbstractPlugin() {
|
|||||||
}
|
}
|
||||||
else -> error("No embedded vision data / fetch url for $name")
|
else -> error("No embedded vision data / fetch url for $name")
|
||||||
}
|
}
|
||||||
|
element.setAttribute(OUTPUT_RENDERED, "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) mapOf(
|
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) mapOf(
|
||||||
@ -254,5 +262,5 @@ public fun runVisionClient(contextBuilder: ContextBuilder.() -> Unit) {
|
|||||||
val visionClient = context.fetch(VisionClient)
|
val visionClient = context.fetch(VisionClient)
|
||||||
window.asDynamic()[RENDER_FUNCTION_NAME] = visionClient::renderAllVisionsById
|
window.asDynamic()[RENDER_FUNCTION_NAME] = visionClient::renderAllVisionsById
|
||||||
|
|
||||||
visionClient.renderAllVisions()
|
//visionClient.renderAllVisions()
|
||||||
}
|
}
|
@ -20,3 +20,7 @@ dependencies {
|
|||||||
exclude(module = "slf4j-simple")
|
exclude(module = "slf4j-simple")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readme{
|
||||||
|
maturity = ru.mipt.npm.gradle.Maturity.PROTOTYPE
|
||||||
|
}
|
@ -31,20 +31,16 @@ public actual class PlotlyPlugin : VisionPlugin(), ElementVisionRenderer {
|
|||||||
override fun render(element: Element, vision: Vision, meta: Meta) {
|
override fun render(element: Element, vision: Vision, meta: Meta) {
|
||||||
val plot = (vision as? VisionOfPlotly)?.plot ?: error("VisionOfPlotly expected but ${vision::class} found")
|
val plot = (vision as? VisionOfPlotly)?.plot ?: error("VisionOfPlotly expected but ${vision::class} found")
|
||||||
val config = PlotlyConfig.read(meta)
|
val config = PlotlyConfig.read(meta)
|
||||||
// println(plot.meta)
|
|
||||||
// println(plot.data[0].toMeta())
|
|
||||||
element.plot(plot, config)
|
element.plot(plot, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun content(target: String): Map<Name, Any> {
|
override fun content(target: String): Map<Name, Any> = when (target) {
|
||||||
return when (target) {
|
|
||||||
ElementVisionRenderer.TYPE -> mapOf("plotly".asName() to this)
|
ElementVisionRenderer.TYPE -> mapOf("plotly".asName() to this)
|
||||||
else -> super.content(target)
|
else -> super.content(target)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public actual companion object : PluginFactory<PlotlyPlugin> {
|
public actual companion object : PluginFactory<PlotlyPlugin> {
|
||||||
override val tag: PluginTag = PluginTag("vision.plotly", PluginTag.DATAFORGE_GROUP)
|
override val tag: PluginTag = PluginTag("vision.plotly.js", PluginTag.DATAFORGE_GROUP)
|
||||||
override val type: KClass<PlotlyPlugin> = PlotlyPlugin::class
|
override val type: KClass<PlotlyPlugin> = PlotlyPlugin::class
|
||||||
override fun invoke(meta: Meta, context: Context): PlotlyPlugin = PlotlyPlugin()
|
override fun invoke(meta: Meta, context: Context): PlotlyPlugin = PlotlyPlugin()
|
||||||
}
|
}
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package space.kscience.visionforge.plotly
|
|
||||||
|
|
||||||
//internal val plotlyScriptLocation = "js/visionforge-three.js"
|
|
||||||
//
|
|
||||||
///**
|
|
||||||
// * A header that stores/embeds plotly bundle and registers plotly renderer in the frontend
|
|
||||||
// */
|
|
||||||
//@OptIn(DFExperimental::class)
|
|
||||||
//public fun plotlyHeader(location: ResourceLocation, filePath: Path? = null): HtmlFragment = {
|
|
||||||
// scriptHeader(
|
|
||||||
// plotlyScriptLocation,
|
|
||||||
// resourceLocation = location,
|
|
||||||
// htmlPath = filePath
|
|
||||||
// ).invoke(this)
|
|
||||||
// script {
|
|
||||||
// type = "text/javascript"
|
|
||||||
// unsafe {
|
|
||||||
// //language=JavaScript
|
|
||||||
// +"space.kscience.visionforge.plotly.loadPlotly()"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
@ -9,7 +9,7 @@ import space.kscience.dataforge.meta.Meta
|
|||||||
import space.kscience.visionforge.VisionPlugin
|
import space.kscience.visionforge.VisionPlugin
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
public actual class PlotlyPlugin : VisionPlugin(), Plugin {
|
public actual class PlotlyPlugin : VisionPlugin() {
|
||||||
|
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
|
@ -3,7 +3,9 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kscience{
|
kscience{
|
||||||
useSerialization()
|
useSerialization{
|
||||||
|
json()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@ -15,3 +17,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readme{
|
||||||
|
maturity = ru.mipt.npm.gradle.Maturity.DEVELOPMENT
|
||||||
|
}
|
40
visionforge-tables/build.gradle.kts
Normal file
40
visionforge-tables/build.gradle.kts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
plugins {
|
||||||
|
id("ru.mipt.npm.gradle.mpp")
|
||||||
|
}
|
||||||
|
|
||||||
|
val tablesVersion = "0.1.4"
|
||||||
|
|
||||||
|
kscience {
|
||||||
|
useSerialization()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
js {
|
||||||
|
useCommonJs()
|
||||||
|
binaries.library()
|
||||||
|
browser{
|
||||||
|
commonWebpackConfig{
|
||||||
|
cssSupport.enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api(project(":visionforge-core"))
|
||||||
|
api("space.kscience:tables-kt:${tablesVersion}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
implementation(npm("tabulator-tables", "5.0.1"))
|
||||||
|
implementation(npm("@types/tabulator-tables", "5.0.1"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readme{
|
||||||
|
maturity = ru.mipt.npm.gradle.Maturity.PROTOTYPE
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package space.kscience.visionforge.tables
|
||||||
|
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.polymorphic
|
||||||
|
import kotlinx.serialization.modules.subclass
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.PluginFactory
|
||||||
|
import space.kscience.dataforge.context.PluginTag
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.visionforge.Vision
|
||||||
|
import space.kscience.visionforge.VisionManager
|
||||||
|
import space.kscience.visionforge.VisionPlugin
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
public class TableVisionPlugin : VisionPlugin() {
|
||||||
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
|
override val visionSerializersModule: SerializersModule
|
||||||
|
get() = SerializersModule {
|
||||||
|
polymorphic(Vision::class) {
|
||||||
|
subclass(VisionOfTable.serializer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object : PluginFactory<TableVisionPlugin> {
|
||||||
|
override val tag: PluginTag = PluginTag("vision.table", PluginTag.DATAFORGE_GROUP)
|
||||||
|
override val type: KClass<TableVisionPlugin> = TableVisionPlugin::class
|
||||||
|
override fun invoke(meta: Meta, context: Context): TableVisionPlugin = TableVisionPlugin()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package space.kscience.visionforge.tables
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import space.kscience.dataforge.meta.*
|
||||||
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
|
import space.kscience.dataforge.values.Null
|
||||||
|
import space.kscience.dataforge.values.Value
|
||||||
|
import space.kscience.dataforge.values.asValue
|
||||||
|
import space.kscience.tables.*
|
||||||
|
import space.kscience.visionforge.VisionBase
|
||||||
|
import space.kscience.visionforge.html.VisionOutput
|
||||||
|
import kotlin.jvm.JvmName
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
|
internal object ColumnHeaderSerializer : KSerializer<ColumnHeader<Value>> {
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor get() = MetaSerializer.descriptor
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): ColumnHeader<Value> {
|
||||||
|
val meta = decoder.decodeSerializableValue(MetaSerializer)
|
||||||
|
return SimpleColumnHeader(meta["name"].string!!, typeOf<Value>(), meta["meta"] ?: Meta.EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: ColumnHeader<Value>) {
|
||||||
|
val meta = Meta {
|
||||||
|
"name" put value.name
|
||||||
|
"meta" put value.meta
|
||||||
|
}
|
||||||
|
encoder.encodeSerializableValue(MetaSerializer, meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public val ColumnHeader<Value>.properties: ValueColumnScheme get() = ValueColumnScheme.read(meta)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("vision.table")
|
||||||
|
public class VisionOfTable(
|
||||||
|
override val headers: List<@Serializable(ColumnHeaderSerializer::class) ColumnHeader<Value>>,
|
||||||
|
) : VisionBase(), Rows<Value> {
|
||||||
|
|
||||||
|
public var data: List<Meta>
|
||||||
|
get() = meta.getIndexed("rows").entries.sortedBy { it.key?.toInt() }.map { it.value }
|
||||||
|
set(value) {
|
||||||
|
meta["rows"] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
public val rows: List<MetaRow> get() = data.map(::MetaRow)
|
||||||
|
|
||||||
|
override fun rowSequence(): Sequence<Row<Value>> = rows.asSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a table to a serializable vision
|
||||||
|
*/
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
public fun <T> Table<T>.toVision(
|
||||||
|
converter: (T?) -> Value,
|
||||||
|
): VisionOfTable = VisionOfTable(headers as TableHeader<Value>).also { vision ->
|
||||||
|
vision.data = rows.map { row ->
|
||||||
|
if (row is MetaRow) {
|
||||||
|
row.meta
|
||||||
|
} else {
|
||||||
|
Meta {
|
||||||
|
headers.forEach {
|
||||||
|
it.name put converter(row[it.name])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmName("valueTableToVision")
|
||||||
|
public fun Table<Value>.toVision(): VisionOfTable = toVision { it ?: Null }
|
||||||
|
|
||||||
|
@JvmName("stringTableToVision")
|
||||||
|
public fun Table<String>.toVision(): VisionOfTable = toVision { (it ?: "").asValue() }
|
||||||
|
|
||||||
|
@JvmName("numberTableToVision")
|
||||||
|
public fun Table<Number>.toVision(): VisionOfTable = toVision { (it ?: Double.NaN).asValue() }
|
||||||
|
|
||||||
|
@DFExperimental
|
||||||
|
public inline fun VisionOutput.table(
|
||||||
|
vararg headers: ColumnHeader<Value>,
|
||||||
|
block: MutableRowTable<Value>.() -> Unit,
|
||||||
|
): VisionOfTable = RowTable(*headers, block = block).toVision()
|
||||||
|
|
||||||
|
@DFExperimental
|
||||||
|
public inline fun VisionOutput.columnTable(
|
||||||
|
columnSize: UInt,
|
||||||
|
block: MutableColumnTable<Value>.() -> Unit,
|
||||||
|
): VisionOfTable = ColumnTable(columnSize, block).toVision()
|
||||||
|
|
||||||
|
@DFExperimental
|
||||||
|
public fun VisionOutput.columnTable(
|
||||||
|
vararg dataAndHeaders: Pair<ColumnHeader<Value>, List<Any?>>,
|
||||||
|
): VisionOfTable {
|
||||||
|
val columns = dataAndHeaders.map { (header, data) ->
|
||||||
|
ListColumn(header, data.map { Value.of(it) })
|
||||||
|
}
|
||||||
|
return ColumnTable(columns).toVision()
|
||||||
|
}
|
||||||
|
|
||||||
|
//public val tabulatorCssHader: HtmlFragment = {
|
||||||
|
// link {
|
||||||
|
// href = "https://unpkg.com/tabulator-tables@5.0.10/dist/css/tabulator.min.css"
|
||||||
|
// rel = "stylesheet"
|
||||||
|
// }
|
||||||
|
//}
|
@ -0,0 +1,32 @@
|
|||||||
|
package space.kscience.visionforge.tables
|
||||||
|
|
||||||
|
import space.kscience.dataforge.values.Value
|
||||||
|
import space.kscience.dataforge.values.asValue
|
||||||
|
import space.kscience.dataforge.values.double
|
||||||
|
import space.kscience.dataforge.values.int
|
||||||
|
import space.kscience.tables.ColumnHeader
|
||||||
|
import space.kscience.tables.ColumnTable
|
||||||
|
import space.kscience.tables.get
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
internal class VisionOfTableTest {
|
||||||
|
@Test
|
||||||
|
fun tableSerialization() {
|
||||||
|
val x by ColumnHeader.typed<Value>()
|
||||||
|
val y by ColumnHeader.typed<Value>()
|
||||||
|
|
||||||
|
val table = ColumnTable<Value>(100U) {
|
||||||
|
x.fill { it.asValue() }
|
||||||
|
y.values = x.values.map { it?.double?.pow(2)?.asValue() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val vision = table.toVision()
|
||||||
|
//println(Json.encodeToString(VisionOfTable.serializer(), table.toVision()))
|
||||||
|
|
||||||
|
val rows = vision.rowSequence().toList()
|
||||||
|
|
||||||
|
assertEquals(50, rows[50][x]?.int)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package space.kscience.visionforge.tables
|
||||||
|
|
||||||
|
import kotlinext.js.jso
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.HTMLElement
|
||||||
|
import space.kscience.dataforge.context.AbstractPlugin
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.PluginFactory
|
||||||
|
import space.kscience.dataforge.context.PluginTag
|
||||||
|
import space.kscience.dataforge.meta.DynamicMeta
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.toDynamic
|
||||||
|
import space.kscience.dataforge.names.Name
|
||||||
|
import space.kscience.dataforge.names.asName
|
||||||
|
import space.kscience.visionforge.ElementVisionRenderer
|
||||||
|
import space.kscience.visionforge.Vision
|
||||||
|
import space.kscience.visionforge.VisionClient
|
||||||
|
import tabulator.Tabulator
|
||||||
|
import tabulator.TabulatorFull
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
public class TableVisionJsPlugin : AbstractPlugin(), ElementVisionRenderer {
|
||||||
|
public val visionClient: VisionClient by require(VisionClient)
|
||||||
|
public val tablesBase: TableVisionPlugin by require(TableVisionPlugin)
|
||||||
|
|
||||||
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
|
override fun attach(context: Context) {
|
||||||
|
super.attach(context)
|
||||||
|
kotlinext.js.require("tabulator-tables/dist/css/tabulator.min.css")
|
||||||
|
kotlinext.js.require("tabulator-tables/src/js/modules/ResizeColumns/ResizeColumns.js")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun rateVision(vision: Vision): Int = when (vision) {
|
||||||
|
is VisionOfTable -> ElementVisionRenderer.DEFAULT_RATING
|
||||||
|
else -> ElementVisionRenderer.ZERO_RATING
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun render(element: Element, vision: Vision, meta: Meta) {
|
||||||
|
val table: VisionOfTable = (vision as? VisionOfTable)
|
||||||
|
?: error("VisionOfTable expected but ${vision::class} found")
|
||||||
|
|
||||||
|
val tableOptions = jso<Tabulator.Options> {
|
||||||
|
columns = table.headers.map { header ->
|
||||||
|
jso<Tabulator.ColumnDefinition> {
|
||||||
|
field = header.name
|
||||||
|
title = header.properties.title ?: header.name
|
||||||
|
resizable = true
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
columns = Array(table.headers.size + 1){
|
||||||
|
if(it==0){
|
||||||
|
jso {
|
||||||
|
field = "@index"
|
||||||
|
title = "#"
|
||||||
|
resizable = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val header = table.headers[it-1]
|
||||||
|
jso {
|
||||||
|
field = header.name
|
||||||
|
title = header.properties.title ?: header.name
|
||||||
|
resizable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data = table.rows.mapIndexed { index, row->
|
||||||
|
val d = row.meta.toDynamic()
|
||||||
|
d["@index"] = index
|
||||||
|
d
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
//layout = "fitColumns"
|
||||||
|
|
||||||
|
pagination = true
|
||||||
|
paginationSize = 10
|
||||||
|
paginationSizeSelector = arrayOf(10, 25, 50, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
TabulatorFull(element as HTMLElement, tableOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun content(target: String): Map<Name, Any> = when (target) {
|
||||||
|
ElementVisionRenderer.TYPE -> mapOf("table".asName() to this)
|
||||||
|
else -> super.content(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object : PluginFactory<TableVisionJsPlugin> {
|
||||||
|
override val tag: PluginTag = PluginTag("vision.table.js", PluginTag.DATAFORGE_GROUP)
|
||||||
|
override val type: KClass<TableVisionJsPlugin> = TableVisionJsPlugin::class
|
||||||
|
override fun invoke(meta: Meta, context: Context): TableVisionJsPlugin = TableVisionJsPlugin()
|
||||||
|
}
|
||||||
|
}
|
2347
visionforge-tables/src/jsMain/kotlin/tabulator/Tabulator.kt
Normal file
2347
visionforge-tables/src/jsMain/kotlin/tabulator/Tabulator.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
|||||||
|
@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE_WARNING")
|
||||||
|
|
||||||
|
package tabulator
|
||||||
|
|
||||||
|
import org.w3c.dom.events.UIEvent
|
||||||
|
|
||||||
|
@Suppress("UNUSED_TYPEALIAS_PARAMETER")
|
||||||
|
internal typealias Pick<T, K> = Any
|
||||||
|
|
||||||
|
@Suppress("UNUSED_TYPEALIAS_PARAMETER")
|
||||||
|
internal typealias Record<K, T> = Any
|
||||||
|
|
||||||
|
internal typealias FilterFunction = (field: String, type: String /* "=" | "!=" | "like" | "<" | ">" | "<=" | ">=" | "in" | "regex" | "starts" | "ends" */, value: Any, filterParams: Tabulator.FilterParams) -> Unit
|
||||||
|
|
||||||
|
internal typealias GroupValuesArg = Array<Array<Any>>
|
||||||
|
|
||||||
|
internal typealias CustomMutator = (value: Any, data: Any, type: String /* "data" | "edit" */, mutatorParams: Any, cell: Tabulator.CellComponent) -> Any
|
||||||
|
|
||||||
|
internal typealias CustomAccessor = (value: Any, data: Any, type: String /* "data" | "download" | "clipboard" */, AccessorParams: Any, column: Tabulator.ColumnComponent, row: Tabulator.RowComponent) -> Any
|
||||||
|
|
||||||
|
internal typealias ColumnCalcParams = (values: Any, data: Any) -> Any
|
||||||
|
|
||||||
|
internal typealias ValueStringCallback = (value: Any) -> String
|
||||||
|
|
||||||
|
internal typealias ValueBooleanCallback = (value: Any) -> Boolean
|
||||||
|
|
||||||
|
internal typealias ValueVoidCallback = (value: Any) -> Unit
|
||||||
|
|
||||||
|
internal typealias EmptyCallback = (callback: () -> Unit) -> Unit
|
||||||
|
|
||||||
|
internal typealias CellEventCallback = (e: UIEvent, cell: Tabulator.CellComponent) -> Unit
|
||||||
|
|
||||||
|
internal typealias CellEditEventCallback = (cell: Tabulator.CellComponent) -> Unit
|
||||||
|
|
||||||
|
internal typealias ColumnEventCallback = (e: UIEvent, column: Tabulator.ColumnComponent) -> Unit
|
||||||
|
|
||||||
|
internal typealias RowEventCallback = (e: UIEvent, row: Tabulator.RowComponent) -> Unit
|
||||||
|
|
||||||
|
internal typealias RowChangedCallback = (row: Tabulator.RowComponent) -> Unit
|
||||||
|
|
||||||
|
internal typealias GroupEventCallback = (e: UIEvent, group: Tabulator.GroupComponent) -> Unit
|
||||||
|
|
||||||
|
internal typealias JSONRecord = Record<String, dynamic /* String | Number | Boolean */>
|
||||||
|
|
||||||
|
internal typealias ColumnSorterParamLookupFunction = (column: Tabulator.ColumnComponent, dir: String /* "asc" | "desc" */) -> Any
|
@ -5,6 +5,7 @@ import info.laht.threekt.core.Object3D
|
|||||||
import info.laht.threekt.geometries.TextBufferGeometry
|
import info.laht.threekt.geometries.TextBufferGeometry
|
||||||
import info.laht.threekt.objects.Mesh
|
import info.laht.threekt.objects.Mesh
|
||||||
import kotlinext.js.jsObject
|
import kotlinext.js.jsObject
|
||||||
|
import kotlinext.js.jso
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.context.warn
|
import space.kscience.dataforge.context.warn
|
||||||
import space.kscience.visionforge.onPropertyChange
|
import space.kscience.visionforge.onPropertyChange
|
||||||
@ -18,7 +19,7 @@ public object ThreeLabelFactory : ThreeFactory<SolidLabel> {
|
|||||||
override val type: KClass<in SolidLabel> get() = SolidLabel::class
|
override val type: KClass<in SolidLabel> get() = SolidLabel::class
|
||||||
|
|
||||||
override fun invoke(three: ThreePlugin, obj: SolidLabel): Object3D {
|
override fun invoke(three: ThreePlugin, obj: SolidLabel): Object3D {
|
||||||
val textGeo = TextBufferGeometry(obj.text, jsObject {
|
val textGeo = TextBufferGeometry(obj.text, jso {
|
||||||
font = obj.fontFamily
|
font = obj.fontFamily
|
||||||
size = 20
|
size = 20
|
||||||
height = 1
|
height = 1
|
||||||
|
Loading…
Reference in New Issue
Block a user