22 Commits

Author SHA1 Message Date
16bfb5b005 Replace VisionTagConsumer by context parameter 2025-10-16 10:56:10 +03:00
56061d1da0 Replace VisionTagConsumer by context parameter 2025-10-16 10:55:16 +03:00
99862f52b8 WIP replace VisionTagConsumer by context parameter 2025-10-15 22:14:13 +03:00
e1fbfc7aa3 Fix tests 2025-10-14 19:46:06 +03:00
ca9db2585d Fix tests 2025-10-14 16:49:48 +03:00
a4bd3bdb58 Fix plotly dynamic render 2025-10-14 16:49:38 +03:00
144058a881 Merge branch 'refs/heads/kotlin/2.2.20' into dev 2025-09-29 09:37:22 +03:00
5079bb448c update gradle version 2025-08-20 09:54:51 +03:00
4da02e29ce Update to 2.2.20-Beta2 2025-07-30 11:57:13 +03:00
9389d3a032 fix legend orientation constats for Plotly 2025-06-16 22:16:27 +03:00
7e3a51d4a4 fix legend orientation constats for Plotly 2025-06-16 22:15:56 +03:00
e708caf584 Add manual coroutine scope for plotly JS render. 2025-06-15 13:06:46 +03:00
bae747b601 Add custom classes to vision div configuration 2025-06-15 13:06:18 +03:00
7f05e5a556 Fix vision listening 2025-06-14 17:59:11 +03:00
29ab796807 Add plotly-kt-server publication 2025-03-21 20:02:39 +03:00
953c696956 Add plotly-kt-server publication 2025-03-21 19:53:02 +03:00
5f285ff7a1 Merge branch 'master' into dev 2025-03-21 11:10:16 +03:00
67dea7c3f1 Merge remote-tracking branch 'spc/dev' into dev 2025-03-21 11:06:59 +03:00
a42edb44f9 Post-release fix 2025-03-21 11:06:50 +03:00
447831707d Merge pull request 'Post-release fix' (!82) from dev into master
Reviewed-on: #82
2025-03-21 10:55:37 +03:00
c70f5a1704 Merge branch 'master' into dev 2025-03-21 10:54:56 +03:00
198b3707c2 Post-release fix 2025-03-21 10:52:00 +03:00
70 changed files with 715 additions and 898 deletions

View File

@@ -5,12 +5,14 @@
### Added
### Changed
- Replaced `VisionTagConsumer` with a proper `VisionHtmlContext` as a context parameter.
### Deprecated
### Removed
### Fixed
- Fix the problem where property listeners do not react on property child node changa
### Security

View File

@@ -1,4 +1,4 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import space.kscience.gradle.useApache2Licence
import space.kscience.gradle.useSPCTeam
@@ -11,23 +11,25 @@ val dataforgeVersion by extra("0.10.1")
allprojects {
group = "space.kscience"
version = "0.5.0"
version = "0.6.0-dev-1"
}
subprojects {
if (name.startsWith("visionforge")) apply<MavenPublishPlugin>()
repositories {
mavenLocal()
maven("https://repo.kotlin.link")
mavenCentral()
maven("https://maven.jzy3d.org/releases")
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
// repositories {
// mavenLocal()
// maven("https://repo.kotlin.link")
// mavenCentral()
// maven("https://maven.jzy3d.org/releases")
// maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
// }
tasks.withType<KotlinCompile> {
compilerOptions {
freeCompilerArgs.addAll("-Xcontext-parameters")
plugins.withId("org.jetbrains.kotlin.multiplatform") {
extensions.configure<KotlinMultiplatformExtension>{
compilerOptions{
freeCompilerArgs.addAll("-Xcontext-parameters")
}
}
}

View File

@@ -10,7 +10,7 @@ group = "ru.mipt.npm"
kscience {
fullStack(
"muon-monitor.js",
development = true,
development = false,
jvmConfig = {
binaries {
executable {

View File

@@ -1,6 +1,6 @@
plugins {
kotlin("multiplatform")
kotlin("jupyter.api")
alias(spclibs.plugins.kotlin.jupyter.api)
id("com.gradleup.shadow") version "8.3.6"
}

View File

@@ -9,6 +9,7 @@ import space.kscience.dataforge.meta.*
import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.meta
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.*
import java.util.zip.ZipInputStream
import kotlin.io.path.Path

View File

@@ -10,6 +10,7 @@ import space.kscience.plotly.models.scatter
import space.kscience.plotly.plotly
import space.kscience.tables.ColumnHeader
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.markup.markdown
import space.kscience.visionforge.solid.axes
import space.kscience.visionforge.solid.box

View File

@@ -14,6 +14,7 @@ import space.kscience.kmath.geometry.euclidean3d.RotationOrder
import space.kscience.kmath.geometry.euclidean3d.fromEuler
import space.kscience.kmath.geometry.euclidean3d.fromRotation
import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.*
import kotlin.math.PI
import kotlin.math.cos

View File

@@ -3,6 +3,7 @@ package space.kscience.visionforge.examples
import space.kscience.kmath.geometry.euclidean3d.Float64Space3D
import space.kscience.kmath.geometry.radians
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.*
import kotlin.math.PI

View File

@@ -15,8 +15,10 @@ import space.kscience.plotly.PlotlyPlugin
import space.kscience.plotly.layout
import space.kscience.plotly.models.Trace
import space.kscience.plotly.models.invoke
import space.kscience.plotly.plot
import space.kscience.plotly.plotly
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.visionPage
import kotlin.math.PI
@@ -51,15 +53,14 @@ suspend fun main() {
h1 { +"This is the plot page" }
a("/other") { +"The other page" }
vision {
plotly {
traces(sinTrace, cosTrace)
layout {
title = "Other dynamic plot"
xaxis.title = "x axis name"
yaxis.title = "y axis name"
}
plot {
traces(sinTrace, cosTrace)
layout {
title = "Other dynamic plot"
xaxis.title = "x axis name"
yaxis.title = "y axis name"
}
}
}

View File

@@ -1,5 +1,6 @@
package space.kscience.visionforge.examples
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.ambientLight
import space.kscience.visionforge.solid.extruded
import space.kscience.visionforge.solid.polygon

View File

@@ -10,6 +10,7 @@ import space.kscience.dataforge.context.request
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.html.visionOfForm
import space.kscience.visionforge.onPropertyChange
import space.kscience.visionforge.server.close

View File

@@ -3,6 +3,7 @@ package space.kscience.visionforge.examples
import space.kscience.gdml.GdmlShowCase
import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.Solids
fun main() = makeVisionFile(resourceLocation = ResourceLocation.SYSTEM) {

View File

@@ -5,6 +5,7 @@ package space.kscience.visionforge.examples
import space.kscience.gdml.*
import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.Solids
import space.kscience.visionforge.solid.color
import space.kscience.visionforge.solid.invoke
@@ -12,229 +13,230 @@ import space.kscience.visionforge.visible
import java.nio.file.Path
fun main() = makeVisionFile(Path.of("curves.html"), resourceLocation = ResourceLocation.EMBED) {
vision("canvas") {
requirePlugin(Solids)
Gdml {
// geometry variables
val worldSize = 500
vision("canvas") {
requirePlugin(Solids)
Gdml {
// geometry variables
val worldSize = 500
// chamber
val chamberHeight = 30 // length of the chamber
val chamberDiameter = 102 // inner diameter of the copper chamber
val chamberOuterSquareSide = 134 // chamber has a square footprint
val chamberBackplateThickness = 15 // thickness of the backplate of the chamber
// teflon disk
val cathodeTeflonDiskHoleRadius = 15
val cathodeTeflonDiskThickness = 5
val cathodeCopperSupportOuterRadius = 45
val cathodeCopperSupportInnerRadius = 8.5
val cathodeCopperSupportThickness = 1.0
// mylar cathode
val mylarCathodeThickness = 0.004
// patern
val cathodePatternLineWidth = 0.3
val cathodePatternDiskRadius = 4.25
// readout
val chamberTeflonWallThickness = 1
val readoutKaptonThickness = 0.5
val readoutCopperThickness = 0.2
val readoutPlaneSide = 60
structure {
val worldMaterial = materials.composite("G4_AIR")
val worldBox = solids.box(worldSize, worldSize, worldSize, name = "world")
val shieldingMaterial = materials.composite("G4_Pb")
val scintillatorMaterial = materials.composite("BC408")
val captureMaterial = materials.composite("G4_Cd")
// chamber
val chamberHeight = 30 // length of the chamber
val chamberDiameter = 102 // inner diameter of the copper chamber
val chamberOuterSquareSide = 134 // chamber has a square footprint
val chamberBackplateThickness = 15 // thickness of the backplate of the chamber
// teflon disk
val cathodeTeflonDiskHoleRadius = 15
val cathodeTeflonDiskThickness = 5
val cathodeCopperSupportOuterRadius = 45
val cathodeCopperSupportInnerRadius = 8.5
val cathodeCopperSupportThickness = 1.0
// mylar cathode
val mylarCathodeThickness = 0.004
// patern
val cathodePatternLineWidth = 0.3
val cathodePatternDiskRadius = 4.25
// readout
val chamberTeflonWallThickness = 1
val readoutKaptonThickness = 0.5
val readoutCopperThickness = 0.2
val readoutPlaneSide = 60
structure {
val worldMaterial = materials.composite("G4_AIR")
val worldBox = solids.box(worldSize, worldSize, worldSize, name = "world")
val shieldingMaterial = materials.composite("G4_Pb")
val scintillatorMaterial = materials.composite("BC408")
val captureMaterial = materials.composite("G4_Cd")
// chamber
val copperMaterial = materials.composite("G4_Cu")
val chamberSolidBase = solids.box(chamberOuterSquareSide, chamberOuterSquareSide, chamberHeight)
val chamberSolidHole = solids.tube(chamberDiameter / 2, chamberHeight)
val chamberSolid = solids.subtraction(chamberSolidBase, chamberSolidHole)
val chamberBodyVolume = volume(copperMaterial, chamberSolid)
val chamberBackplateSolid =
solids.box(chamberOuterSquareSide, chamberOuterSquareSide, chamberBackplateThickness)
val chamberBackplateVolume = volume(copperMaterial, chamberBackplateSolid)
// chamber teflon walls
val teflonMaterial = materials.composite("G4_TEFLON")
val chamberTeflonWallSolid = solids.tube(chamberDiameter / 2, chamberHeight) {
rmin = chamberDiameter / 2.0 - chamberTeflonWallThickness
}
val chamberTeflonWallVolume = volume(teflonMaterial, chamberTeflonWallSolid)
// cathode
val cathodeCopperDiskMaterial = materials.composite("G4_Cu")
val cathodeWindowMaterial = materials.composite("G4_MYLAR")
val cathodeTeflonDiskSolidBase =
solids.tube(chamberOuterSquareSide / 2, cathodeTeflonDiskThickness) {
rmin = cathodeTeflonDiskHoleRadius
}
val cathodeCopperDiskSolid =
solids.tube(cathodeCopperSupportOuterRadius, cathodeCopperSupportThickness) {
rmin = cathodeCopperSupportInnerRadius
}
val cathodeTeflonDiskSolid = solids.subtraction(cathodeTeflonDiskSolidBase, cathodeCopperDiskSolid)
val cathodeTeflonDiskVolume = volume(teflonMaterial, cathodeTeflonDiskSolid)
val cathodeWindowSolid = solids.tube(cathodeTeflonDiskHoleRadius, mylarCathodeThickness)
val cathodeWindowVolume = volume(cathodeWindowMaterial, cathodeWindowSolid)
val cathodeFillingMaterial = materials.composite("G4_Galactic")
val cathodeFillingSolidBase = solids.tube(cathodeTeflonDiskHoleRadius, cathodeTeflonDiskThickness)
val cathodeFillingSolid = solids.subtraction(cathodeFillingSolidBase, cathodeCopperDiskSolid) {
position(z = chamberHeight / 2 - mylarCathodeThickness / 2)
}
val cathodeFillingVolume = volume(cathodeFillingMaterial, cathodeFillingSolid)
// pattern
val cathodePatternLineAux = solids.box(
cathodePatternLineWidth,
cathodeCopperSupportInnerRadius * 2,
cathodeCopperSupportThickness
)
val cathodePatternCentralHole = solids.tube(
cathodePatternDiskRadius - 0 * cathodePatternLineWidth,
cathodeCopperSupportThickness * 1.1
)
val cathodePatternLine = solids.subtraction(cathodePatternLineAux, cathodePatternCentralHole)
val cathodePatternDisk = solids.tube(
cathodePatternDiskRadius,
cathodeCopperSupportThickness
) { rmin = cathodePatternDiskRadius - cathodePatternLineWidth }
val cathodeCopperDiskSolidAux0 =
solids.union(cathodeCopperDiskSolid, cathodePatternLine) {
rotation(x = 0, y = 0, z = 0)
}
val cathodeCopperDiskSolidAux1 =
solids.union(cathodeCopperDiskSolidAux0, cathodePatternLine) {
rotation = GdmlRotation(
"cathodePatternRotation1", x = 0, y = 0, z = 45
)
}
val cathodeCopperDiskSolidAux2 =
solids.union(cathodeCopperDiskSolidAux1, cathodePatternLine) {
rotation = GdmlRotation(
"cathodePatternRotation2", x = 0, y = 0, z = 90
)
}
val cathodeCopperDiskSolidAux3 =
solids.union(cathodeCopperDiskSolidAux2, cathodePatternLine) {
rotation = GdmlRotation(
"cathodePatternRotation3", x = 0, y = 0, z = 135
)
}
val cathodeCopperDiskFinal =
solids.union(cathodeCopperDiskSolidAux3, cathodePatternDisk)
val cathodeCopperDiskVolume =
volume(cathodeCopperDiskMaterial, cathodeCopperDiskFinal)
val gasSolidOriginal = solids.tube(
chamberDiameter / 2 - chamberTeflonWallThickness,
chamberHeight
)
val kaptonReadoutMaterial = materials.composite("G4_KAPTON")
val kaptonReadoutSolid = solids.box(
chamberOuterSquareSide,
chamberOuterSquareSide,
readoutKaptonThickness)
val kaptonReadoutVolume = volume( kaptonReadoutMaterial, kaptonReadoutSolid)
val copperReadoutSolid =
solids.box(readoutPlaneSide, readoutPlaneSide, readoutCopperThickness)
val copperReadoutVolume = volume(copperMaterial, copperReadoutSolid)
val gasSolidAux =
solids.subtraction(gasSolidOriginal, copperReadoutSolid) {
position(z = -chamberHeight / 2 + readoutCopperThickness / 2)
}
val gasMaterial = materials.composite("G4_Ar")
val gasSolid =
solids.subtraction( gasSolidAux, cathodeWindowSolid) {
position(z = chamberHeight / 2 - mylarCathodeThickness / 2)
rotation(z = 45)
}
val gasVolume = volume(gasMaterial, gasSolid)
// world setup
world = volume(worldMaterial, worldBox) {
physVolume(gasVolume) {
name = "gas"
}
physVolume(kaptonReadoutVolume) {
name = "kaptonReadout"
position {
z = -chamberHeight / 2 - readoutKaptonThickness / 2
}
}
physVolume(copperReadoutVolume) {
name = "copperReadout"
position {
z = -chamberHeight / 2 + readoutCopperThickness / 2
}
rotation { z = 45 }
}
physVolume(chamberBodyVolume) {
name = "chamberBody"
}
physVolume(chamberBackplateVolume) {
name = "chamberBackplate"
position {
z = -chamberHeight / 2 - readoutKaptonThickness - chamberBackplateThickness / 2
}
}
physVolume(chamberTeflonWallVolume) {
name = "chamberTeflonWall"
}
physVolume(cathodeTeflonDiskVolume) {
name = "cathodeTeflonDisk"
position {
z = chamberHeight / 2 + cathodeTeflonDiskThickness / 2
}
}
physVolume(cathodeCopperDiskVolume) {
name = "cathodeCopperDisk"
position {
z = chamberHeight / 2 + cathodeCopperSupportThickness / 2
}
}
physVolume(cathodeWindowVolume) {
name = "cathodeWindow"
position {
z = chamberHeight / 2 - mylarCathodeThickness / 2
}
}
physVolume(cathodeFillingVolume) {
name = "cathodeFilling"
position {
z = chamberHeight / 2 + cathodeTeflonDiskThickness / 2
}
}
}
val copperMaterial = materials.composite("G4_Cu")
val chamberSolidBase = solids.box(chamberOuterSquareSide, chamberOuterSquareSide, chamberHeight)
val chamberSolidHole = solids.tube(chamberDiameter / 2, chamberHeight)
val chamberSolid = solids.subtraction(chamberSolidBase, chamberSolidHole)
val chamberBodyVolume = volume(copperMaterial, chamberSolid)
val chamberBackplateSolid =
solids.box(chamberOuterSquareSide, chamberOuterSquareSide, chamberBackplateThickness)
val chamberBackplateVolume = volume(copperMaterial, chamberBackplateSolid)
// chamber teflon walls
val teflonMaterial = materials.composite("G4_TEFLON")
val chamberTeflonWallSolid = solids.tube(chamberDiameter / 2, chamberHeight) {
rmin = chamberDiameter / 2.0 - chamberTeflonWallThickness
}
}.toVision {
solids { _, solid, _ ->
//disable visibility for the world box
if(solid.name == "world"){
visible = false
val chamberTeflonWallVolume = volume(teflonMaterial, chamberTeflonWallSolid)
// cathode
val cathodeCopperDiskMaterial = materials.composite("G4_Cu")
val cathodeWindowMaterial = materials.composite("G4_MYLAR")
val cathodeTeflonDiskSolidBase =
solids.tube(chamberOuterSquareSide / 2, cathodeTeflonDiskThickness) {
rmin = cathodeTeflonDiskHoleRadius
}
if(solid.name.startsWith("gas")){
color("green")
} else {
//make all solids semi-transparent
transparent()
val cathodeCopperDiskSolid =
solids.tube(cathodeCopperSupportOuterRadius, cathodeCopperSupportThickness) {
rmin = cathodeCopperSupportInnerRadius
}
val cathodeTeflonDiskSolid = solids.subtraction(cathodeTeflonDiskSolidBase, cathodeCopperDiskSolid)
val cathodeTeflonDiskVolume = volume(teflonMaterial, cathodeTeflonDiskSolid)
val cathodeWindowSolid = solids.tube(cathodeTeflonDiskHoleRadius, mylarCathodeThickness)
val cathodeWindowVolume = volume(cathodeWindowMaterial, cathodeWindowSolid)
val cathodeFillingMaterial = materials.composite("G4_Galactic")
val cathodeFillingSolidBase = solids.tube(cathodeTeflonDiskHoleRadius, cathodeTeflonDiskThickness)
val cathodeFillingSolid = solids.subtraction(cathodeFillingSolidBase, cathodeCopperDiskSolid) {
position(z = chamberHeight / 2 - mylarCathodeThickness / 2)
}
val cathodeFillingVolume = volume(cathodeFillingMaterial, cathodeFillingSolid)
// pattern
val cathodePatternLineAux = solids.box(
cathodePatternLineWidth,
cathodeCopperSupportInnerRadius * 2,
cathodeCopperSupportThickness
)
val cathodePatternCentralHole = solids.tube(
cathodePatternDiskRadius - 0 * cathodePatternLineWidth,
cathodeCopperSupportThickness * 1.1
)
val cathodePatternLine = solids.subtraction(cathodePatternLineAux, cathodePatternCentralHole)
val cathodePatternDisk = solids.tube(
cathodePatternDiskRadius,
cathodeCopperSupportThickness
) { rmin = cathodePatternDiskRadius - cathodePatternLineWidth }
val cathodeCopperDiskSolidAux0 =
solids.union(cathodeCopperDiskSolid, cathodePatternLine) {
rotation(x = 0, y = 0, z = 0)
}
val cathodeCopperDiskSolidAux1 =
solids.union(cathodeCopperDiskSolidAux0, cathodePatternLine) {
rotation = GdmlRotation(
"cathodePatternRotation1", x = 0, y = 0, z = 45
)
}
val cathodeCopperDiskSolidAux2 =
solids.union(cathodeCopperDiskSolidAux1, cathodePatternLine) {
rotation = GdmlRotation(
"cathodePatternRotation2", x = 0, y = 0, z = 90
)
}
val cathodeCopperDiskSolidAux3 =
solids.union(cathodeCopperDiskSolidAux2, cathodePatternLine) {
rotation = GdmlRotation(
"cathodePatternRotation3", x = 0, y = 0, z = 135
)
}
val cathodeCopperDiskFinal =
solids.union(cathodeCopperDiskSolidAux3, cathodePatternDisk)
val cathodeCopperDiskVolume =
volume(cathodeCopperDiskMaterial, cathodeCopperDiskFinal)
val gasSolidOriginal = solids.tube(
chamberDiameter / 2 - chamberTeflonWallThickness,
chamberHeight
)
val kaptonReadoutMaterial = materials.composite("G4_KAPTON")
val kaptonReadoutSolid = solids.box(
chamberOuterSquareSide,
chamberOuterSquareSide,
readoutKaptonThickness
)
val kaptonReadoutVolume = volume(kaptonReadoutMaterial, kaptonReadoutSolid)
val copperReadoutSolid =
solids.box(readoutPlaneSide, readoutPlaneSide, readoutCopperThickness)
val copperReadoutVolume = volume(copperMaterial, copperReadoutSolid)
val gasSolidAux =
solids.subtraction(gasSolidOriginal, copperReadoutSolid) {
position(z = -chamberHeight / 2 + readoutCopperThickness / 2)
}
val gasMaterial = materials.composite("G4_Ar")
val gasSolid =
solids.subtraction(gasSolidAux, cathodeWindowSolid) {
position(z = chamberHeight / 2 - mylarCathodeThickness / 2)
rotation(z = 45)
}
val gasVolume = volume(gasMaterial, gasSolid)
// world setup
world = volume(worldMaterial, worldBox) {
physVolume(gasVolume) {
name = "gas"
}
physVolume(kaptonReadoutVolume) {
name = "kaptonReadout"
position {
z = -chamberHeight / 2 - readoutKaptonThickness / 2
}
}
physVolume(copperReadoutVolume) {
name = "copperReadout"
position {
z = -chamberHeight / 2 + readoutCopperThickness / 2
}
rotation { z = 45 }
}
physVolume(chamberBodyVolume) {
name = "chamberBody"
}
physVolume(chamberBackplateVolume) {
name = "chamberBackplate"
position {
z = -chamberHeight / 2 - readoutKaptonThickness - chamberBackplateThickness / 2
}
}
physVolume(chamberTeflonWallVolume) {
name = "chamberTeflonWall"
}
physVolume(cathodeTeflonDiskVolume) {
name = "cathodeTeflonDisk"
position {
z = chamberHeight / 2 + cathodeTeflonDiskThickness / 2
}
}
physVolume(cathodeCopperDiskVolume) {
name = "cathodeCopperDisk"
position {
z = chamberHeight / 2 + cathodeCopperSupportThickness / 2
}
}
physVolume(cathodeWindowVolume) {
name = "cathodeWindow"
position {
z = chamberHeight / 2 - mylarCathodeThickness / 2
}
}
physVolume(cathodeFillingVolume) {
name = "cathodeFilling"
position {
z = chamberHeight / 2 + cathodeTeflonDiskThickness / 2
}
}
}
}
}.toVision {
solids { _, solid, _ ->
//disable visibility for the world box
if (solid.name == "world") {
visible = false
}
if (solid.name.startsWith("gas")) {
color("green")
} else {
//make all solids semi-transparent
transparent()
}
}
}
}
}
}

View File

@@ -2,6 +2,7 @@ package space.kscience.visionforge.examples
import space.kscience.gdml.GdmlShowCase
import space.kscience.visionforge.gdml.gdml
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.Solids
import space.kscience.visionforge.solid.solid

View File

@@ -1,6 +1,7 @@
package space.kscience.visionforge.examples
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.markup.markdown
fun main() = makeVisionFile(resourceLocation = ResourceLocation.SYSTEM) {

View File

@@ -5,6 +5,7 @@ import space.kscience.plotly.Plotly
import space.kscience.plotly.layout
import space.kscience.plotly.models.*
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
fun main() = makeVisionFile(resourceLocation = ResourceLocation.SYSTEM) {
vision {

View File

@@ -4,6 +4,7 @@ import kotlinx.html.div
import kotlinx.html.h1
import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.*
import java.nio.file.Paths
import kotlin.random.Random

View File

@@ -1,6 +1,7 @@
package space.kscience.visionforge.examples
import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.*
import kotlin.math.PI

View File

@@ -1,6 +1,7 @@
package space.kscience.visionforge.examples
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.box
import space.kscience.visionforge.solid.invoke
import space.kscience.visionforge.solid.material

View File

@@ -1,5 +1,6 @@
package space.kscience.visionforge.examples
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.ambientLight
import space.kscience.visionforge.solid.polygon
import space.kscience.visionforge.solid.solid

View File

@@ -4,6 +4,7 @@ import space.kscience.dataforge.meta.ValueType
import space.kscience.tables.ColumnHeader
import space.kscience.tables.valueRow
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.tables.table
import kotlin.math.pow

View File

@@ -15,6 +15,7 @@ import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.meta
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.visionPage

View File

@@ -4,6 +4,7 @@ import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.request
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.*
import space.kscience.visionforge.three.makeThreeJsFile

View File

@@ -1,15 +1,13 @@
package space.kscience.visionforge.solid.demo
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import space.kscience.visionforge.html.startApplication
import space.kscience.visionforge.solid.x
import space.kscience.visionforge.solid.y
import kotlin.random.Random
@OptIn(DelicateCoroutinesApi::class)
fun main() {
startApplication { document ->
val element = document.getElementById("demo") ?: error("Element with id 'demo' not found on page")

View File

@@ -8,4 +8,4 @@ org.gradle.workers.max=4
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
kotlin.native.enableKlibsCrossCompilation=true
toolsVersion=0.17.1-kotlin-2.1.20
toolsVersion=0.19.2-kotlin-2.2.20

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
### Fixed
- Fix legend orientation constants
### Security

View File

@@ -15,12 +15,15 @@ dependencies {
}
kotlin{
jvmToolchain(17)
jvmToolchain(21)
compilerOptions {
freeCompilerArgs.add("-Xcontext-parameters")
}
}
// A workaround for https://youtrack.jetbrains.com/issue/KT-44101
val copyPlotlyResources by tasks.creating(Copy::class){
val copyPlotlyResources by tasks.registering(Copy::class){
dependsOn(":plotly-kt:plotly-kt-server:jvmProcessResources")
mustRunAfter(":plotly-kt:plotly-kt-server:jvmTestProcessResources")
from(project(":plotly-kt:plotly-kt-server").layout.buildDirectory.file("processedResources/jvm/main"))

View File

@@ -13,16 +13,17 @@ import space.kscience.plotly.models.geo.json.GeoJsonFeatureCollection
import space.kscience.plotly.models.geo.json.combine
import space.kscience.plotly.models.geo.openStreetMap
import space.kscience.plotly.plot
import space.kscience.visionforge.html.HtmlVisionContext
import space.kscience.visionforge.plotly.serveSinglePage
import space.kscience.visionforge.server.openInBrowser
import java.net.URL
import java.net.URI
import kotlin.random.Random
suspend fun main() {
//downloading GeoJson
val geoJsonString =
URL("https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/4_kreise/4_niedrig.geo.json").readText()
URI("https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/4_kreise/4_niedrig.geo.json").toURL().readText()
// Filtering GeoJson features and creating new feature set
@@ -50,7 +51,7 @@ suspend fun main() {
locations.numbers = features.map { it.id!!.int }
// Set random values to locations
z.numbers = features.map { Random.nextDouble(1.0, 10.0) }
context.launch {
contextOf<HtmlVisionContext>().context.launch {
while (isActive) {
delay(300)
z.numbers = features.map { Random.nextDouble(1.0, 10.0) }

View File

@@ -10,7 +10,7 @@ import space.kscience.plotly.models.geo.json.GeoJsonFeatureCollection
import space.kscience.plotly.models.geo.json.combine
import space.kscience.plotly.models.geo.openStreetMap
import space.kscience.plotly.openInBrowser
import java.net.URL
import java.net.URI
import kotlin.random.Random
@@ -18,7 +18,9 @@ fun main() {
//downloading GeoJson
val geoJsonString =
URL("https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/4_kreise/4_niedrig.geo.json").readText()
URI("https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/main/4_kreise/4_niedrig.geo.json")
.toURL()
.readText()
// Filtering GeoJson features and creating new feature set

View File

@@ -1,6 +1,6 @@
plugins {
id("space.kscience.gradle.mpp")
kotlin("jupyter.api")
alias(spclibs.plugins.kotlin.jupyter.api)
`maven-publish`
}
@@ -31,9 +31,9 @@ kscience {
}
}
tasks.processJupyterApiResources {
libraryProducers = listOf("space.kscience.plotly.PlotlyIntegration")
}
//tasks.processJupyterApiResources {
// libraryProducers = listOf("space.kscience.plotly.PlotlyIntegration")
//}
kotlin {

View File

@@ -80,8 +80,9 @@ public class Plot : AbstractVision(), MutableVisionGroup<Trace> {
*/
@UnstablePlotlyAPI
@JvmSynchronized
internal fun removeTrace(index: Int) {
public fun removeTrace(index: Int) {
_data.removeAt(index)
emitEvent(VisionGroupCompositionChangedEvent(NameToken("trace", _data.size.toString()), null))
}
override val descriptor: MetaDescriptor get() = Companion.descriptor

View File

@@ -8,9 +8,12 @@ import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.plotly.models.Trace
import space.kscience.visionforge.VisionBuilder
import space.kscience.visionforge.html.*
import space.kscience.visionforge.html.HtmlVisionContext
import space.kscience.visionforge.html.VisionOutput
import space.kscience.visionforge.html.vision
import kotlin.js.JsName
/**
@@ -65,6 +68,21 @@ public class PlotlyConfig : Scheme() {
public var responsive: Boolean? by boolean()
public var imageFormat: String? by string(Name.parse("toImageButtonOptions.format"))
/**
* A list of class names applied to the output `div` in the generated HTML for the plot.
*
* This property allows customization of the CSS classes assigned to the `div` element
* that contains the rendered plot. It can be utilized to add custom styling or specific
* class-based behaviors to the output.
*
* By default, this property is initialized as an empty list and can be updated to include
* necessary class names as strings.
*/
public var classes: List<String> by stringList(
default = emptyArray(),
key = HtmlVisionContext.OUTPUT_DIV_CLASSES_KEY.asName()
)
public fun withEditorButton() {
showEditInChartStudio = true
plotlyServerURL = "https://chart-studio.plotly.com"
@@ -92,12 +110,10 @@ public inline fun VisionOutput.plotly(
return Plotly.plot(block)
}
context(rootConsumer: VisionTagConsumer<*>)
public fun TagConsumer<*>.plot(
context(htmlContext: HtmlVisionContext)
public fun <T> TagConsumer<T>.plot(
config: PlotlyConfig = PlotlyConfig(),
block: Plot.() -> Unit,
): Unit = with(rootConsumer) {
vision {
plotly(config, block)
}
}
): T = vision {
plotly(config, block)
}

View File

@@ -45,7 +45,7 @@ public fun TagConsumer<*>.staticPlot(
config: PlotlyConfig = PlotlyConfig(),
plotId: String = "plotly[${Uuid.random()}]",
plot: Plot.() -> Unit
) = staticPlot(Plotly.plot(plot), config, plotId)
): Unit = staticPlot(Plotly.plot(plot), config, plotId)
/**
* Create an html (including headers) string from plot

View File

@@ -6,14 +6,10 @@ import space.kscience.dataforge.meta.enum
import space.kscience.dataforge.meta.scheme
import space.kscience.plotly.numberGreaterThan
import space.kscience.plotly.numberInRange
import kotlin.js.JsName
public enum class LegendOrientation {
@JsName("v")
vertical,
@JsName("h")
horizontal
v,
h
}
public enum class XAnchor {
@@ -103,7 +99,7 @@ public class Legend : Scheme() {
* Sets the orientation of the legend (vertical/horizontal).
* Default: vertical.
*/
public var orientation: LegendOrientation by enum(LegendOrientation.vertical)
public var orientation: LegendOrientation by enum(LegendOrientation.v)
/**
* The order at which the legend items are displayed.

View File

@@ -1,167 +0,0 @@
package space.kscience.plotly
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.*
import org.w3c.dom.url.URL
import org.w3c.fetch.RequestInit
import kotlin.js.Promise
import kotlin.js.json
@JsExport
public object PlotlyConnect {
/**
* Wait for the Plotly library to be loaded
*/
private fun withPlotly(action: PlotlyJs.() -> Unit) {
if (jsTypeOf(PlotlyJs) !== "undefined") {
action(PlotlyJs);
} else {
val promiseOfPlotly: Promise<PlotlyJs> = window.asDynamic().promiseOfPlotly as Promise<PlotlyJs>
if (jsTypeOf(promiseOfPlotly) != "undefined") {
promiseOfPlotly.then { action(PlotlyJs) }
} else {
console.warn("Plotly not defined. Loading the script from CDN")
window.asDynamic().promiseOfPlotly = Promise<PlotlyJs> { resolve, reject ->
val plotlyLoaderScript = document.createElement("script") as HTMLScriptElement
plotlyLoaderScript.src = "https://cdn.plot.ly/plotly-2.29.0.min.js"
plotlyLoaderScript.type = "text/javascript"
plotlyLoaderScript.onload = {
resolve(PlotlyJs)
action(PlotlyJs)
}
plotlyLoaderScript.onerror = { error, _, _, _, _ ->
console.error(error);
reject(error as Throwable)
}
document.head?.appendChild(plotlyLoaderScript);
}
}
}
}
/**
* Request and parse json from given address
* @param url {URL}
* @param callback
*/
private fun getJSON(url: URL, callback: (dynamic) -> Unit) {
try {
window.fetch(
url,
RequestInit(
method = "GET",
headers = json("Accept" to "application/json")
)
).then { response ->
if (!response.ok) {
error("Fetch request failed with error: ${response.statusText}")
} else {
response.json().then(callback)
}
}.catch { error -> console.log(error) }
} catch (e: Exception) {
window.alert("Fetch of plot data failed with error: $e")
}
}
public fun makePlot(
graphDiv: Element,
data: Array<dynamic>,
layout: dynamic,
config: dynamic,
) {
withPlotly { newPlot(graphDiv, data, layout, config) }
}
/**
* Create a plot taking data from given url
* @param id {string} element id for plot
* @param from {URL} json server url
* @param config {object} plotly configuration
*/
public fun createPlotFrom(id: String, from: URL, config: dynamic = {}) {
getJSON(from) { json ->
val element = document.getElementById(id) as HTMLElement
withPlotly { newPlot(element, json.data, json.layout, config) }
}
}
/**
* Update a plot taking data from given url
* @param id {string} element id for plot
* @param from {URL} json server url
* @return {JSON}
*/
public fun updatePlotFrom(id: String, from: URL) {
getJSON(from) { json ->
val element = document.getElementById(id) as HTMLElement
withPlotly { react(element, json.data, json.layout) }
}
}
/**
* Start pull updates with regular requests from client side
* @param id {string}
* @param from
* @param millis
*/
public fun startPull(id: String, from: URL, millis: Int) {
window.setInterval({ updatePlotFrom(id, from) }, millis)
}
/**
* Start push updates via websocket
* @param id {string} element id for plot
* @param ws {URL} a websocket address
*/
public fun startPush(id: String, ws: String) {
val element = document.getElementById(id) as HTMLElement
val socket = WebSocket(ws)
socket.onopen = {
console.log("[Plotly.kt] A connection for plot with id = $id with server established on $ws")
}
socket.onclose = { event ->
event as CloseEvent
if (event.wasClean) {
console.log("The connection with server is closed")
} else {
console.log("The connection with server is broken") // Server process is dead
}
console.log("Code: ${event.code} case: ${event.reason}")
}
socket.onerror = { error ->
error as ErrorEvent
console.error("Plotly push update error: " + error.message)
socket.close()
}
socket.onmessage = { event ->
val json: dynamic = JSON.parse(event.data.toString())
if (json.plotId === id) {
if (json.contentType === "layout") {
withPlotly { relayout(element, json.content) }
} else if (json.contentType === "trace") {
withPlotly { restyle(element, json.content, json.trace) }
}
}
}
//gracefully close the socket just in case
window.onbeforeunload = {
console.log("Gracefully closing socket")
socket.close()
null
}
}
}
public fun main() {
window.asDynamic().plotlyConnect = PlotlyConnect
window.asDynamic().Plotly = PlotlyJs
}

View File

@@ -1,5 +1,6 @@
package space.kscience.plotly
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.filterIsInstance
@@ -32,7 +33,11 @@ private fun List<MetaRepr>.toDynamic(): Array<dynamic> = map { it.toDynamic() }.
* Attach a plot to this element or update the existing plot
*/
@OptIn(DelicateCoroutinesApi::class)
public fun Element.plot(plotlyConfig: PlotlyConfig = PlotlyConfig(), plot: Plot) {
public fun Element.plot(
plotlyConfig: PlotlyConfig,
plot: Plot,
scope: CoroutineScope = plot.manager?.context ?: GlobalScope
) {
// console.info("""
// Plotly.react(
@@ -51,7 +56,7 @@ public fun Element.plot(plotlyConfig: PlotlyConfig = PlotlyConfig(), plot: Plot)
)
//start updates
val listenJob = (plot.manager?.context ?: GlobalScope).launch {
val listenJob = scope.launch {
plot.data.forEachIndexed { index, trace ->
trace.eventFlow.filterIsInstance<VisionPropertyChangedEvent>().onEach { event ->
val traceData = trace.toDynamic()
@@ -92,27 +97,34 @@ public fun Element.plot(plot: Plot, plotlyConfig: PlotlyConfig = PlotlyConfig())
/**
* Create a plot in this element
*/
public inline fun Element.plot(plotlyConfig: PlotlyConfig = PlotlyConfig(), plotBuilder: Plot.() -> Unit) {
plot(plotlyConfig, Plot().apply(plotBuilder))
public inline fun Element.plot(
scope: CoroutineScope,
plotlyConfig: PlotlyConfig = PlotlyConfig(),
plotBuilder: Plot.() -> Unit
) {
plot(plotlyConfig, Plot().apply(plotBuilder), scope)
}
public class PlotlyElement(public val div: HTMLElement)
/**
* Create a div element and render plot in it
* Create a div element and render the plot in it
*/
@OptIn(DelicateCoroutinesApi::class)
public fun TagConsumer<HTMLElement>.plotDiv(
plotlyConfig: PlotlyConfig = PlotlyConfig(),
plotlyConfig: PlotlyConfig,
plot: Plot,
scope: CoroutineScope = plot.manager?.context ?: GlobalScope,
): PlotlyElement = PlotlyElement(div("plotly-kt-plot").apply { plot(plotlyConfig, plot) })
/**
* Render plot in the HTML element using direct plotly API.
*/
public inline fun TagConsumer<HTMLElement>.plotDiv(
scope: CoroutineScope,
plotlyConfig: PlotlyConfig = PlotlyConfig(),
plotBuilder: Plot.() -> Unit,
): PlotlyElement = PlotlyElement(div("plotly-kt-plot").apply { plot(plotlyConfig, plotBuilder) })
): PlotlyElement = PlotlyElement(div("plotly-kt-plot").apply { plot(scope, plotlyConfig, plotBuilder) })
@OptIn(ExperimentalSerializationApi::class)
public fun PlotlyElement.on(eventType: PlotlyEventListenerType, block: MouseEvent.(PlotlyEvent) -> Unit) {

View File

@@ -1,5 +1,6 @@
package space.kscience.plotly
import kotlinx.browser.window
import kotlinx.serialization.modules.SerializersModule
import org.w3c.dom.Element
import space.kscience.dataforge.context.Context
@@ -12,6 +13,7 @@ import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionPlugin
import space.kscience.visionforge.html.ElementVisionRenderer
import space.kscience.visionforge.html.JsVisionClient
import space.kscience.visionforge.html.renderAllVisions
public class PlotlyJsPlugin : VisionPlugin(), ElementVisionRenderer {
public val plotly: PlotlyPlugin by require(PlotlyPlugin)
@@ -39,10 +41,25 @@ public class PlotlyJsPlugin : VisionPlugin(), ElementVisionRenderer {
else -> super.content(target)
}
public companion object : PluginFactory<PlotlyJsPlugin> {
public companion object : PluginFactory<PlotlyJsPlugin> {
override val tag: PluginTag = PluginTag("vision.plotly.js", PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): PlotlyJsPlugin = PlotlyJsPlugin()
public val defaultClient: JsVisionClient by lazy {
val context = Context("Plotly-kt") {
plugin(PlotlyJsPlugin)
}
context.plugins[PlotlyJsPlugin]!!.visionClient
}
}
}
public fun main() {
window.asDynamic().Plotly = PlotlyJs
window.asDynamic().renderAllPlots = {
PlotlyJsPlugin.defaultClient.renderAllVisions()
}
}

View File

@@ -1,5 +1,7 @@
package space.kscience.plotly
import kotlinx.html.script
import kotlinx.html.unsafe
import space.kscience.visionforge.html.*
import space.kscience.visionforge.visionManager
import java.awt.Desktop
@@ -53,6 +55,17 @@ public fun Plotly.makePageFile(
resourceLocation,
actualPath
),
"plotly-render" to HtmlFragment {
script {
defer = true
unsafe {
+"""
window.renderAllPlots()
""".trimIndent()
}
}
}
) + additionalHeaders
}
if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI())

View File

@@ -2,6 +2,7 @@ plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose.compiler)
alias(spclibs.plugins.compose.jb)
`maven-publish`
}
kscience {
@@ -26,6 +27,7 @@ kscience {
jvmMain {
api(projects.visionforgeServer)
api(project.dependencies.platform(spclibs.ktor.bom))
api("io.ktor:ktor-server-cio")
}
}

View File

@@ -7,11 +7,11 @@ pluginManagement {
val toolsVersion: String by extra
repositories {
mavenLocal()
maven("https://repo.kotlin.link")
mavenCentral()
gradlePluginPortal()
mavenCentral()
maven("https://repo.kotlin.link")
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
mavenLocal()
}
plugins {
@@ -26,9 +26,10 @@ dependencyResolutionManagement {
val toolsVersion: String by extra
repositories {
mavenLocal()
maven("https://repo.kotlin.link")
mavenCentral()
maven("https://repo.kotlin.link")
maven("https://maven.jzy3d.org/releases")
mavenLocal()
}
versionCatalogs {

View File

@@ -22,7 +22,7 @@ kotlin {
jsMain {
dependencies {
api("app.softwork:bootstrap-compose:0.3.0")
api("app.softwork:bootstrap-compose:0.3.1")
api("app.softwork:bootstrap-compose-icons:0.3.0")
// implementation(npm("bootstrap", "5.3.3"))
// implementation(npm(" bootstrap-icons", "1.11.3"))

View File

@@ -28,9 +28,8 @@ public interface ComposeHtmlVisionRenderer : ElementVisionRenderer {
public companion object
}
/**
* A compose-html renderer for a vision of given type
* A compose-html renderer for a vision of a given type
*/
public inline fun <reified T : Vision> ComposeHtmlVisionRenderer(
acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING,

View File

@@ -0,0 +1,18 @@
package space.kscience.visionforge.html
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.flow.map
import space.kscience.visionforge.Vision
import space.kscience.visionforge.flowProperty
import kotlin.reflect.KProperty1
@Composable
public fun <V : Vision, T> V.collectPropertyAsState(
property: KProperty1<V, T>,
propertyName: String = property.name,
): State<T> = flowProperty(propertyName)
.map { property.get(this@collectPropertyAsState) }
.collectAsState(property.get(this))

View File

@@ -16,8 +16,8 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.*
import space.kscience.visionforge.html.HtmlVisionContext
import space.kscience.visionforge.html.VisionOutput
import space.kscience.visionforge.html.VisionTagConsumer
/**
* A Kotlin-browser plugin that renders visions based on provided renderers and governs communication with the server.
@@ -116,7 +116,7 @@ public fun Vision(
name: Name? = null,
meta: Meta = Meta.EMPTY,
) {
val actualName = name ?: NameToken(VisionTagConsumer.DEFAULT_VISION_NAME, vision.hashCode().toUInt().toString()).asName()
val actualName = name ?: NameToken(HtmlVisionContext.DEFAULT_VISION_NAME, vision.hashCode().toUInt().toString()).asName()
context.request(ComposeVisionClient).renderVision(actualName, vision, meta)
}
@@ -127,7 +127,7 @@ public fun Vision(
meta: Meta = Meta.EMPTY,
buildOutput: VisionOutput.() -> Vision,
) {
val actualName = name ?: NameToken(VisionTagConsumer.DEFAULT_VISION_NAME, buildOutput.hashCode().toUInt().toString()).asName()
val actualName = name ?: NameToken(HtmlVisionContext.DEFAULT_VISION_NAME, buildOutput.hashCode().toUInt().toString()).asName()
val output = VisionOutput(context, actualName)
val vision = output.buildOutput()
context.request(ComposeVisionClient).renderVision(actualName, vision, meta)

View File

@@ -7,7 +7,7 @@ val dataforgeVersion: String by rootProject.extra
kscience {
jvm()
js()
native {}
native()
// wasm()
useCoroutines()
commonMain {

View File

@@ -118,7 +118,7 @@ public interface MutableVision : Vision {
override suspend fun receiveEvent(event: VisionEvent) {
if (event is VisionChange) {
if (event.children?.isNotEmpty() == true) {
error("Vision is not a group")
error("Received vision group change event, but $this Vision does not handle children changes")
}
event.properties?.let {
updateProperties(it, Name.EMPTY)
@@ -130,7 +130,7 @@ public interface MutableVision : Vision {
name: Name,
inherited: Boolean = isInheritedProperty(name),
useStyles: Boolean = isStyledProperty(name),
): MutableMeta = properties.getOrCreate(name).withDefault { suffix->
): MutableMeta = properties.getOrCreate(name).withDefault { suffix ->
val propertyName = name + suffix
if (useStyles) getStyleProperty(propertyName)?.let { return@withDefault it }
if (inherited) parent?.readProperty(propertyName, inherited, useStyles)?.let { return@withDefault it }

View File

@@ -0,0 +1,201 @@
package space.kscience.visionforge.html
import kotlinx.html.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.*
import space.kscience.visionforge.html.HtmlVisionContext.Companion.DEFAULT_VISION_NAME
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_CLASS
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_DIV_CLASSES_KEY
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_META_CLASS
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_NAME_ATTRIBUTE
/**
* A placeholder object to attach inline vision builders.
*/
@VisionBuilder
public class VisionOutput(override val context: Context, public val name: Name) : ContextAware {
public var meta: Meta = Meta.EMPTY
private val requirements: MutableSet<PluginFactory<*>> = HashSet()
public fun requirePlugin(factory: PluginFactory<*>) {
requirements.add(factory)
}
public val visionManager: VisionManager
get() = if (requirements.all { req -> context.plugins.find(true) { it.tag == req.tag } != null }) {
context.visionManager
} else {
val newContext = context.buildContext(NameToken(DEFAULT_VISION_NAME, name.toString()).asName()) {
plugin(VisionManager)
requirements.forEach { plugin(it) }
}
newContext.visionManager
}
}
public inline fun VisionOutput.meta(block: MutableMeta.() -> Unit) {
this.meta = Meta(block)
}
public fun VisionOutput.meta(metaRepr: MetaRepr) {
this.meta = metaRepr.toMeta()
}
/**
* Modified scope that allows rendering output fragments and visions in them
*/
@VisionBuilder
public abstract class HtmlVisionContext(
override val context: Context,
private val idPrefix: String? = null,
) : ContextAware {
public open fun resolveId(name: Name): String = (idPrefix ?: "output") + "[$name]"
/**
* Render a vision inside the output fragment
* @param manager a [VisionManager] to be used in renderer
* @param name name of the output container
* @param vision an object to be rendered
* @param outputMeta optional configuration for the output container
*/
public abstract fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta)
public companion object {
public const val OUTPUT_CLASS: String = "visionforge-output"
public const val OUTPUT_META_CLASS: String = "visionforge-output-meta"
public const val OUTPUT_DATA_CLASS: String = "visionforge-output-data"
public const val OUTPUT_DIV_CLASSES_KEY: String = "classes"
public const val OUTPUT_FETCH_ATTRIBUTE: String = "data-output-fetch"
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_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
public const val DEFAULT_ENDPOINT: String = "."
public const val AUTO_DATA_ATTRIBUTE: String = "@auto"
public const val DEFAULT_VISION_NAME: String = "vision"
}
}
/**
* Create a placeholder for a vision output with optional [Vision] in it
*/
context(htmlContext: HtmlVisionContext)
public fun <T> TagConsumer<T>.addVision(
name: Name,
manager: VisionManager,
vision: Vision?,
outputMeta: Meta = Meta.EMPTY,
): T = if (vision == null) div {
+"Empty Vision output"
} else div {
id = htmlContext.resolveId(name)
classes = setOf(OUTPUT_CLASS, *(outputMeta[OUTPUT_DIV_CLASSES_KEY].stringList?.toTypedArray() ?: emptyArray()))
if (vision.parent == null) {
vision.setAsRoot(manager)
}
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
with(htmlContext) {
renderVision(manager, name, vision, outputMeta)
}
if (!outputMeta.isEmpty()) {
//Hard-code output configuration
script {
type = "text/json"
attributes["class"] = OUTPUT_META_CLASS
unsafe {
+("\n" + manager.jsonFormat.encodeToString(MetaSerializer, outputMeta) + "\n")
}
}
}
}
@VisionBuilder
context(htmlContext: HtmlVisionContext)
public fun <T> TagConsumer<T>.vision(
vision: Vision,
name: Name? = null,
outputMeta: Meta = Meta.EMPTY,
) {
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, vision.hashCode().toUInt().toString()).asName()
addVision(actualName, htmlContext.context.visionManager, vision, outputMeta)
}
@VisionBuilder
context(htmlContext: HtmlVisionContext)
private fun <T> TagConsumer<T>.vision(
visionManager: VisionManager,
name: Name,
vision: Vision,
outputMeta: Meta = Meta.EMPTY,
): T = div {
id = htmlContext.resolveId(name)
classes = setOf(OUTPUT_CLASS)
vision.setAsRoot(visionManager)
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
with(htmlContext) {
renderVision(visionManager, name, vision, outputMeta)
}
if (!outputMeta.isEmpty()) {
//Hard-code output configuration
script {
attributes["class"] = OUTPUT_META_CLASS
unsafe {
+visionManager.jsonFormat.encodeToString(MetaSerializer, outputMeta)
}
}
}
}
@VisionBuilder
context(htmlContext: HtmlVisionContext)
private fun <T> TagConsumer<T>.vision(
name: Name,
vision: Vision,
outputMeta: Meta = Meta.EMPTY,
): T = vision(htmlContext.context.visionManager, name, vision, outputMeta)
/**
* Insert a vision in this HTML.
*/
@VisionBuilder
context(htmlContext: HtmlVisionContext)
public fun <T> TagConsumer<T>.vision(
name: Name? = null,
visionProvider: VisionOutput.() -> Vision,
): T {
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, visionProvider.hashCode().toUInt().toString()).asName()
val output = VisionOutput(htmlContext.context, actualName)
val vision = output.visionProvider()
return vision(output.visionManager, actualName, vision, output.meta)
}
/**
* Insert a vision in this HTML.
*/
@VisionBuilder
context(htmlContext: HtmlVisionContext)
public fun <T> TagConsumer<T>.vision(
name: String?,
visionProvider: VisionOutput.() -> Vision,
): T = vision(name?.parseAsName(), visionProvider)

View File

@@ -3,16 +3,15 @@ package space.kscience.visionforge.html
import kotlinx.html.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
public fun interface HtmlVisionFragment {
public fun VisionTagConsumer<*>.append()
context(htmlContext: HtmlVisionContext) public fun TagConsumer<*>.append()
}
public fun HtmlVisionFragment.appendTo(consumer: VisionTagConsumer<*>): Unit = consumer.append()
context(scope: HtmlVisionContext)
public fun HtmlVisionFragment.appendTo(consumer: TagConsumer<*>): Unit = consumer.append()
public data class VisionDisplay(val visionManager: VisionManager, val vision: Vision, val meta: Meta)
@@ -36,23 +35,7 @@ public fun TagConsumer<*>.visionFragment(
fragment: HtmlVisionFragment,
) {
val consumer = object : VisionTagConsumer<Any?>(this@visionFragment, visionManager, idPrefix) {
override fun <T> TagConsumer<T>.vision(name: Name?, buildOutput: VisionOutput.() -> Vision): T {
//Avoid re-creating cached visions
val actualName = name ?: NameToken(
DEFAULT_VISION_NAME,
buildOutput.hashCode().toString(16)
).asName()
val display = displayCache.getOrPut(actualName) {
val output = VisionOutput(context, actualName)
val vision = output.buildOutput()
VisionDisplay(output.visionManager, vision, output.meta)
}
return addVision(actualName, display.visionManager, display.vision, display.meta)
}
val consumer = object : HtmlVisionContext(visionManager.context, idPrefix) {
override fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta) {
@@ -79,7 +62,9 @@ public fun TagConsumer<*>.visionFragment(
}
}
fragment.appendTo(consumer)
with(consumer) {
fragment.appendTo(this@visionFragment)
}
}
public fun FlowContent.visionFragment(

View File

@@ -1,171 +0,0 @@
package space.kscience.visionforge.html
import kotlinx.html.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionTagConsumer.Companion.DEFAULT_VISION_NAME
import space.kscience.visionforge.setAsRoot
import space.kscience.visionforge.visionManager
import kotlin.collections.set
@DslMarker
public annotation class VisionDSL
/**
* A placeholder object to attach inline vision builders.
*/
@VisionDSL
public class VisionOutput(override val context: Context, public val name: Name): ContextAware {
public var meta: Meta = Meta.EMPTY
private val requirements: MutableSet<PluginFactory<*>> = HashSet()
public fun requirePlugin(factory: PluginFactory<*>) {
requirements.add(factory)
}
public val visionManager: VisionManager
get() = if (requirements.all { req -> context.plugins.find(true) { it.tag == req.tag } != null }) {
context.visionManager
} else {
val newContext = context.buildContext(NameToken(DEFAULT_VISION_NAME, name.toString()).asName()) {
plugin(VisionManager)
requirements.forEach { plugin(it) }
}
newContext.visionManager
}
}
public inline fun VisionOutput.meta(block: MutableMeta.() -> Unit) {
this.meta = Meta(block)
}
public fun VisionOutput.meta(metaRepr: MetaRepr) {
this.meta = metaRepr.toMeta()
}
/**
* Modified [TagConsumer] that allows rendering output fragments and visions in them
*/
@VisionDSL
public abstract class VisionTagConsumer<R>(
private val root: TagConsumer<R>,
public val visionManager: VisionManager,
private val idPrefix: String? = null,
) : TagConsumer<R> by root, ContextAware {
override val context: Context get() = visionManager.context
public open fun resolveId(name: Name): String = (idPrefix ?: "output") + "[$name]"
/**
* Render a vision inside the output fragment
* @param manager a [VisionManager] to be used in renderer
* @param name name of the output container
* @param vision an object to be rendered
* @param outputMeta optional configuration for the output container
*/
protected abstract fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta)
/**
* Create a placeholder for a vision output with optional [Vision] in it
* TODO with multi-receivers could be replaced by [VisionTagConsumer, TagConsumer] extension
*/
protected fun <T> TagConsumer<T>.addVision(
name: Name,
manager: VisionManager,
vision: Vision?,
outputMeta: Meta = Meta.EMPTY,
): T = if (vision == null) div {
+"Empty Vision output"
} else div {
id = resolveId(name)
classes = setOf(OUTPUT_CLASS)
if (vision.parent == null) {
vision.setAsRoot(manager)
}
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
renderVision(manager, name, vision, outputMeta)
if (!outputMeta.isEmpty()) {
//Hard-code output configuration
script {
type = "text/json"
attributes["class"] = OUTPUT_META_CLASS
unsafe {
+("\n" + manager.jsonFormat.encodeToString(MetaSerializer, outputMeta) + "\n")
}
}
}
}
/**
* Insert a vision in this HTML.
* TODO replace by multi-receiver
*/
@VisionDSL
public open fun <T> TagConsumer<T>.vision(
name: Name? = null,
buildOutput: VisionOutput.() -> Vision,
): T {
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, buildOutput.hashCode().toUInt().toString()).asName()
val output = VisionOutput(context, actualName)
val vision = output.buildOutput()
return addVision(actualName, output.visionManager, vision, output.meta)
}
/**
* TODO to be replaced by multi-receiver
*/
@VisionDSL
public fun <T> TagConsumer<T>.vision(
name: String?,
buildOutput: VisionOutput.() -> Vision,
): T = vision(name?.parseAsName(), buildOutput)
@VisionDSL
public open fun <T> TagConsumer<T>.vision(
vision: Vision,
name: Name? = null,
outputMeta: Meta = Meta.EMPTY,
) {
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, vision.hashCode().toUInt().toString()).asName()
addVision(actualName, context.visionManager, vision, outputMeta)
}
/**
* Process the resulting object produced by [TagConsumer]
*/
protected open fun processResult(result: R) {
//do nothing by default
}
override fun finalize(): R = root.finalize().also { processResult(it) }
public companion object {
public const val OUTPUT_CLASS: String = "visionforge-output"
public const val OUTPUT_META_CLASS: String = "visionforge-output-meta"
public const val OUTPUT_DATA_CLASS: String = "visionforge-output-data"
public const val OUTPUT_FETCH_ATTRIBUTE: String = "data-output-fetch"
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_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
public const val DEFAULT_ENDPOINT: String = "."
public const val AUTO_DATA_ATTRIBUTE: String = "@auto"
public const val DEFAULT_VISION_NAME: String = "vision"
}
}

View File

@@ -9,6 +9,7 @@ import space.kscience.dataforge.meta.Value
import space.kscience.dataforge.meta.descriptors.get
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.startsWith
import kotlin.reflect.KProperty1
private fun Vision.withAncestors(): List<Vision> = buildList {
@@ -42,7 +43,7 @@ public fun Vision.flowProperty(
}
combinedFlow.filterIsInstance<VisionPropertyChangedEvent>().collect { event ->
if (event.propertyName == propertyName || (useStyles && event.propertyName == Vision.STYLE_KEY)) {
if (event.propertyName.startsWith(propertyName) || (useStyles && event.propertyName == Vision.STYLE_KEY)) {
emit(readProperty(event.propertyName, inherited, useStyles))
}
}
@@ -89,7 +90,7 @@ public fun Vision.useProperty(
} else {
eventFlow
}.filterIsInstance<VisionPropertyChangedEvent>().onEach { event ->
if (event.propertyName == propertyName || (useStyles && event.propertyName == Vision.STYLE_KEY)) {
if (event.propertyName.startsWith(propertyName) || (useStyles && event.propertyName == Vision.STYLE_KEY)) {
callback(readProperty(event.propertyName, inherited, useStyles))
}
}.collect()
@@ -134,7 +135,7 @@ public fun <V : Vision, T> V.onPropertyChange(
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: suspend V.(T) -> Unit,
): Job = inheritedEventFlow().filterIsInstance<VisionPropertyChangedEvent>().onEach {
if (it.propertyName.toString() == property.name) {
if (it.propertyName.startsWith(property.name)) {
callback(property.get(this))
}
}.launchIn(scope)

View File

@@ -9,25 +9,25 @@ import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionGroup
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.visionManager
import kotlin.collections.set
import kotlin.test.Test
typealias HtmlVisionRenderer = FlowContent.(name: Name, vision: Vision, meta: Meta) -> Unit
fun FlowContent.renderVisionFragment(
renderer: DIV.(name: Name, vision: Vision, meta: Meta) -> Unit,
internal fun FlowContent.renderVisionFragment(
renderer: FlowContent.(name: Name, vision: Vision, meta: Meta) -> Unit,
idPrefix: String? = null,
fragment: HtmlVisionFragment,
): Map<Name, Vision> {
val visionMap = HashMap<Name, Vision>()
val consumer = object : VisionTagConsumer<Any?>(consumer, Global.visionManager, idPrefix) {
val visionContext = object : HtmlVisionContext(Global, idPrefix) {
override fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta) {
visionMap[name] = vision
renderer(name, vision, outputMeta)
}
}
fragment.appendTo(consumer)
context(visionContext) {
fragment.appendTo(consumer)
}
return visionMap
}

View File

@@ -1,10 +1,6 @@
package space.kscience.visionforge.meta
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
@@ -13,7 +9,6 @@ import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.get
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.*
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.milliseconds
@@ -93,51 +88,4 @@ internal class VisionPropertyTest {
subscription.cancel()
}
@Test
@Ignore
fun testChildrenPropertyFlow() = runTest(timeout = 500.milliseconds) {
val group = SimpleVisionGroup().apply {
properties {
"test" put 11
}
group("child") {
properties {
"test" put 22
}
}
}
val child = group.visions["child"] as MutableVision
val semaphore = Semaphore(1, 1)
val changesFlow = child.flowPropertyValue("test", inherited = true).map {
semaphore.release()
it!!.int
}
val collectedValues = ArrayList<Int>(5)
val collectorJob = changesFlow.onEach {
collectedValues.add(it)
}.launchIn(this)
assertEquals(22, child.readProperty("test", true).int)
semaphore.acquire()
child.properties.remove("test")
assertEquals(11, child.readProperty("test", true).int)
semaphore.acquire()
group.properties["test"] = 33
assertEquals(33, child.readProperty("test", true).int)
semaphore.acquire()
collectorJob.cancel()
assertEquals(listOf(22, 11, 33), collectedValues)
}
}

View File

@@ -12,7 +12,6 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.encodeToString
import org.w3c.dom.*
import org.w3c.dom.url.URL
import space.kscience.dataforge.context.*
@@ -24,11 +23,11 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.*
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_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_NAME_ATTRIBUTE
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_RENDERED
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_CONNECT_ATTRIBUTE
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_FETCH_ATTRIBUTE
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_NAME_ATTRIBUTE
import space.kscience.visionforge.html.HtmlVisionContext.Companion.OUTPUT_RENDERED
import kotlin.time.Duration.Companion.milliseconds
/**
@@ -94,7 +93,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
private fun startVisionUpdate(element: Element, visionName: Name, vision: Vision, outputMeta: Meta) {
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
val wsUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) {
val wsUrl = if (attr.value.isBlank() || attr.value == HtmlVisionContext.AUTO_DATA_ATTRIBUTE) {
val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
@@ -194,10 +193,10 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
}
/**
* 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 [HtmlVisionContext.OUTPUT_CLASS] class.
*/
public fun renderVisionIn(element: Element) {
if (!element.classList.contains(VisionTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element")
if (!element.classList.contains(HtmlVisionContext.OUTPUT_CLASS)) error("The element $element is not an output element")
val name = resolveName(element)?.parseAsName() ?: error("The element is not a vision output")
if (element.attributes[OUTPUT_RENDERED]?.value == "true") {
@@ -207,7 +206,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
logger.info { "Rendering VF output with name $name" }
}
val outputMeta = element.getEmbeddedData(VisionTagConsumer.OUTPUT_META_CLASS)?.let {
val outputMeta = element.getEmbeddedData(HtmlVisionContext.OUTPUT_META_CLASS)?.let {
VisionManager.defaultJson.decodeFromString(MetaSerializer, it).also {
logger.info { "Output meta for $name: $it" }
}
@@ -218,7 +217,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
element.attributes[OUTPUT_FETCH_ATTRIBUTE] != null -> {
val attr = element.attributes[OUTPUT_FETCH_ATTRIBUTE]!!
val fetchUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) {
val fetchUrl = if (attr.value.isBlank() || attr.value == HtmlVisionContext.AUTO_DATA_ATTRIBUTE) {
val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
@@ -244,9 +243,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
}
// use embedded data if it is available
element.getElementsByClassName(VisionTagConsumer.OUTPUT_DATA_CLASS).length > 0 -> {
element.getElementsByClassName(HtmlVisionContext.OUTPUT_DATA_CLASS).length > 0 -> {
//Getting embedded vision data
val embeddedVision = element.getEmbeddedData(VisionTagConsumer.OUTPUT_DATA_CLASS)!!.let {
val embeddedVision = element.getEmbeddedData(HtmlVisionContext.OUTPUT_DATA_CLASS)!!.let {
visionManager.decodeFromString(it)
}
logger.info { "Found embedded vision data with name $name" }
@@ -292,10 +291,10 @@ private fun whenDocumentLoaded(block: Document.() -> Unit): Unit {
}
/**
* Fetch and render visions for all elements with [VisionTagConsumer.OUTPUT_CLASS] class inside given [element].
* Fetch and render visions for all elements with [HtmlVisionContext.OUTPUT_CLASS] class inside given [element].
*/
public fun JsVisionClient.renderAllVisionsIn(element: Element) {
val elements = element.getElementsByClassName(VisionTagConsumer.OUTPUT_CLASS)
val elements = element.getElementsByClassName(HtmlVisionContext.OUTPUT_CLASS)
logger.info { "Finished search for outputs. Found ${elements.length} items" }
elements.asList().forEach { child ->
renderVisionIn(child)
@@ -316,7 +315,7 @@ public fun JsVisionClient.renderAllVisionsById(document: Document, id: String):
/**
* Fetch visions from the server for all elements with [VisionTagConsumer.OUTPUT_CLASS] class in the document body
* Fetch visions from the server for all elements with [HtmlVisionContext.OUTPUT_CLASS] class in the document body
*/
public fun JsVisionClient.renderAllVisions(): Unit = whenDocumentLoaded {
val element = body ?: error("Document does not have a body")

View File

@@ -1,97 +0,0 @@
package space.kscience.visionforge.html
import kotlinx.html.*
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.isEmpty
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionTagConsumer.Companion.DEFAULT_VISION_NAME
import space.kscience.visionforge.setAsRoot
import space.kscience.visionforge.visionManager
/**
* Rendering context for visions in HTML
*/
public interface HtmlVisionContext : ContextAware {
/**
* Generate div id for vision div tag
*/
public fun generateId(name: Name): String = "vision[$name]"
/**
* Render vision at given [DIV]
*/
public fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta)
}
//public typealias HtmlVisionContextFragment = context(htmlContext: HtmlVisionContext) TagConsumer<*>.() -> Unit
//context(HtmlVisionContext)
//public fun HtmlVisionFragment(
// content: TagConsumer<*>.() -> Unit,
//): HtmlVisionFragment = HtmlVisionFragment { }
context(htmlContext: HtmlVisionContext)
private fun <T> TagConsumer<T>.vision(
visionManager: VisionManager,
name: Name,
vision: Vision,
outputMeta: Meta = Meta.EMPTY,
): T = div {
id = htmlContext.generateId(name)
classes = setOf(VisionTagConsumer.OUTPUT_CLASS)
vision.setAsRoot(visionManager)
attributes[VisionTagConsumer.OUTPUT_NAME_ATTRIBUTE] = name.toString()
if (!outputMeta.isEmpty()) {
//Hard-code output configuration
script {
attributes["class"] = VisionTagConsumer.OUTPUT_META_CLASS
unsafe {
+visionManager.jsonFormat.encodeToString(MetaSerializer, outputMeta)
}
}
}
with(htmlContext) {
renderVision(name, vision, outputMeta)
}
}
context(htmlContext: HtmlVisionContext)
private fun <T> TagConsumer<T>.vision(
name: Name,
vision: Vision,
outputMeta: Meta = Meta.EMPTY,
): T = vision(htmlContext.context.visionManager, name, vision, outputMeta)
/**
* Insert a vision in this HTML.
*/
context(htmlContext: HtmlVisionContext)
@VisionDSL
public fun <T> TagConsumer<T>.vision(
name: Name? = null,
visionProvider: VisionOutput.() -> Vision,
): T {
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, visionProvider.hashCode().toUInt().toString()).asName()
val output = VisionOutput(htmlContext.context, actualName)
val vision = output.visionProvider()
return vision(output.visionManager, actualName, vision, output.meta)
}
/**
* Insert a vision in this HTML.
*/
context(htmlContext: HtmlVisionContext)
@VisionDSL
public fun <T> TagConsumer<T>.vision(
name: String?,
visionProvider: VisionOutput.() -> Vision,
): T = vision(name?.parseAsName(), visionProvider)

View File

@@ -3,6 +3,7 @@ package space.kscience.visionforge.meta
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
@@ -38,40 +39,31 @@ internal class PropertyFlowTest {
val changesFlow = child.flowProperty("test", inherited = true)
// child.inheritedEventFlow().filterIsInstance<VisionPropertyChangedEvent>().onEach { event ->
// println(event)
// delay(2)
// println(child.readProperty("test", inherited = true))
// }.launchIn(this)
val collectedValues = ArrayList<Int>(5)
val collectorJob = changesFlow.onEach {
changesFlow.onEach {
collectedValues.add(it.int!!)
}.launchIn(this)
}.launchIn(backgroundScope)
delay(2)
delay(1)
assertEquals(22, child.readProperty("test", true).int)
// assertEquals(1, collectedValues.size)
parent.properties["test1"] = 88 // another property
child.properties.remove("test")
delay(2)
delay(1)
assertEquals(11, child.readProperty("test", true).int)
// assertEquals(2, collectedValues.size)
parent.properties["test"] = 33
delay(2)
delay(1)
assertEquals(33, child.readProperty("test", true).int)
// assertEquals(3, collectedValues.size)
collectorJob.cancel()
assertEquals(listOf(22, 11, 33), collectedValues)
advanceUntilIdle()
//assertEquals(listOf(22, 11, 33), collectedValues)
assertEquals(22, collectedValues.first())
assertEquals(33, collectedValues.last())
println("finished")
}
}

View File

@@ -1,5 +1,7 @@
plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.kotlin.jupyter.api)
}
description = "Common visionforge jupyter module"
@@ -7,19 +9,18 @@ description = "Common visionforge jupyter module"
kscience {
jvm()
js()
jupyterLibrary()
dependencies {
api(projects.visionforgeCore)
}
dependencies(jvmMain){
api(projects.visionforgeServer)
api(project.dependencies.platform(spclibs.ktor.bom))
api("io.ktor:ktor-server-cio-jvm")
api("io.ktor:ktor-server-websockets-jvm")
api("io.ktor:ktor-server-cors-jvm")
api(projects.visionforgeServer)
}
}
readme {
maturity = space.kscience.gradle.Maturity.EXPERIMENTAL
}

View File

@@ -118,7 +118,7 @@ public fun VisionForge.html(body: TagConsumer<*>.() -> Unit): MimeTypedResult =
/**
* Create a fragment without a head to be embedded in the page
*/
public fun VisionForge.fragment(body: VisionTagConsumer<*>.() -> Unit): MimeTypedResult = produceHtml(false, body)
public fun VisionForge.fragment(body: HtmlVisionFragment): MimeTypedResult = produceHtml(false, body)
/**
@@ -126,6 +126,6 @@ public fun VisionForge.fragment(body: VisionTagConsumer<*>.() -> Unit): MimeType
*/
public fun VisionForge.page(
pageHeaders: Map<String, HtmlFragment> = emptyMap(),
body: VisionTagConsumer<*>.() -> Unit,
body: HtmlVisionFragment,
): VisionPage = VisionPage(visionManager, pageHeaders, body)

View File

@@ -1,5 +1,6 @@
plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.kotlin.jupyter.api)
}
description = "Jupyter api artifact including all common modules"
@@ -33,10 +34,13 @@ kscience {
jsMain {
implementation(projects.visionforgeThreejs)
}
jupyterLibrary("space.kscience.visionforge.jupyter.JupyterCommonIntegration")
}
//tasks.processJupyterApiResources {
// libraryProducers = listOf("space.kscience.visionforge.jupyter.JupyterCommonIntegration")
//}
readme {
maturity = space.kscience.gradle.Maturity.EXPERIMENTAL
}

View File

@@ -7,6 +7,7 @@ import space.kscience.plotly.Plot
import space.kscience.plotly.PlotlyPlugin
import space.kscience.tables.Table
import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.markup.MarkupPlugin
import space.kscience.visionforge.solid.Solids
import space.kscience.visionforge.tables.TableVisionPlugin

View File

@@ -6,6 +6,7 @@ kscience{
jvm()
jvmMain {
api(projects.visionforgeCore)
api(project.dependencies.platform(spclibs.ktor.bom))
api("io.ktor:ktor-server-host-common")
api("io.ktor:ktor-server-html-builder")
api("io.ktor:ktor-server-websockets")

View File

@@ -413,6 +413,7 @@ public final class space/kscience/visionforge/solid/HexagonKt {
public abstract class space/kscience/visionforge/solid/LightSource : space/kscience/visionforge/solid/MiscSolid {
public static final field Companion Lspace/kscience/visionforge/solid/LightSource$Companion;
public static final field DEFAULT_INTENSITY D
public fun <init> ()V
public synthetic fun <init> (ILspace/kscience/dataforge/meta/ObservableMutableMeta;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public final fun getColor ()Lspace/kscience/visionforge/solid/ColorAccessor;

View File

@@ -2,7 +2,7 @@ plugins {
id("space.kscience.gradle.mpp")
}
val kmathVersion = "0.4.1"
val kmathVersion = "0.4.2"
kscience {
jvm()

View File

@@ -117,7 +117,7 @@ public operator fun SolidGroup.get(name: Name): Solid? = getVision(name)
public operator fun SolidGroup.get(name: String): Solid? = getVision(name)
public operator fun SolidGroup.set(name: NameToken, value: Solid?) = setVision(name, value)
public operator fun SolidGroup.set(name: NameToken, value: Solid?): Unit = setVision(name, value)
public operator fun SolidGroup.set(name: Name, vision: Solid?) {
when (name.length) {
@@ -143,7 +143,7 @@ public operator fun SolidGroup.set(name: Name, vision: Solid?) {
}
}
public operator fun SolidGroup.set(name: String, vision: Solid?) = set(name.parseAsName(), vision)
public operator fun SolidGroup.set(name: String, vision: Solid?): Unit = set(name.parseAsName(), vision)
/**
* Add anonymous (auto-assigned name) child to a SolidGroup

View File

@@ -1,5 +1,6 @@
package space.kscience.visionforge.solid
import kotlinx.html.TagConsumer
import kotlinx.serialization.PolymorphicSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.PolymorphicModuleBuilder
@@ -13,7 +14,9 @@ import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.NameToken
import space.kscience.visionforge.*
import space.kscience.visionforge.html.HtmlVisionContext
import space.kscience.visionforge.html.VisionOutput
import space.kscience.visionforge.html.vision
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
@@ -100,3 +103,15 @@ public inline fun VisionOutput.solid(options: Canvas3DOptions? = null, block: So
@VisionBuilder
public inline fun VisionOutput.solid(options: Canvas3DOptions.() -> Unit, block: SolidGroup.() -> Unit): SolidGroup =
solid(Canvas3DOptions(options), block)
@VisionBuilder
context(htmlContext: HtmlVisionContext)
public fun <T> TagConsumer<T>.solid(
name: String? = null,
options: Canvas3DOptions? = null,
block: SolidGroup.() -> Unit
): T = vision(name) {
requirePlugin(Solids)
solid(options = options, block = block)
}

View File

@@ -1,13 +1,11 @@
package space.kscience.visionforge.solid
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.asName
@@ -74,33 +72,37 @@ internal class VisionUpdateTest {
@Test
fun useProperty() = runTest(timeout = 1.seconds) {
withContext(Dispatchers.Default) {
val group = testSolids.solidGroup {
box(100, 100, 100)
}
val box = group.visions.values.first()
val collected = Channel<String?>(5)
box.useProperty(SolidMaterial.MATERIAL_COLOR_KEY) {
println(it.string)
collected.send(it.string)
}
delay(1)
group.color("red")
group.color("green")
box.color("blue")
assertEquals("blue", box.readProperty(SolidMaterial.MATERIAL_COLOR_KEY).string)
assertEquals("blue", box.color.string)
val list = collected.consumeAsFlow().take(4).toList()
assertEquals(null, list.first())
assertEquals("blue", list.last())
val group = testSolids.solidGroup {
box(100, 100, 100)
}
val box = group.visions.values.first()
val collected = Channel<String?>(5)
box.useProperty(
propertyName = SolidMaterial.MATERIAL_COLOR_KEY,
scope = backgroundScope
) {
println(it.string)
collected.send(it.string)
}
delay(1)
group.color("red")
group.color("green")
box.color("blue")
delay(1)
assertEquals("blue", box.readProperty(SolidMaterial.MATERIAL_COLOR_KEY).string)
assertEquals("blue", box.color.string)
val list = collected.consumeAsFlow().take(4).toList()
assertEquals(null, list.first())
assertEquals("blue", list.last())
}
}

View File

@@ -1,6 +1,6 @@
package space.kscience.visionforge.tables
import js.objects.jso
import js.objects.unsafeJso
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import space.kscience.dataforge.context.AbstractPlugin
@@ -39,17 +39,17 @@ public class TableVisionJsPlugin : AbstractPlugin(), ElementVisionRenderer {
val table: VisionOfTable = (vision as? VisionOfTable)
?: error("VisionOfTable expected but ${vision::class} found")
val tableOptions = jso<Options> {
val tableOptions = unsafeJso<Options> {
columns = Array(table.headers.size + 1) {
if (it == 0) {
jso {
unsafeJso {
field = "@index"
title = "#"
resizable = false
}
} else {
val header = table.headers[it - 1]
jso {
unsafeJso {
field = header.name
title = header.properties.title ?: header.name
resizable = true

View File

@@ -1,7 +1,7 @@
package space.kscience.visionforge.solid.three
import js.objects.jso
import js.objects.unsafeJso
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.context.warn
import space.kscience.visionforge.onPropertyChange
@@ -18,7 +18,7 @@ public object ThreeLabelFactory : ThreeFactory<SolidLabel> {
override val type: KClass<in SolidLabel> get() = SolidLabel::class
override suspend fun build(three: ThreePlugin, vision: SolidLabel, observe: Boolean): Object3D {
val textGeo = TextBufferGeometry(vision.text, jso {
val textGeo = TextBufferGeometry(vision.text, unsafeJso {
font = vision.fontFamily
size = 20
height = 1

View File

@@ -1,6 +1,5 @@
package space.kscience.visionforge.three
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.*
import space.kscience.visionforge.solid.Solids
import java.awt.Desktop
@@ -9,7 +8,6 @@ import java.nio.file.Path
public val VisionPage.Companion.threeJsHeader: HtmlFragment get() = scriptHeader("js/visionforge-three.js")
@DFExperimental
public fun Solids.makeThreeJsFile(
path: Path? = null,
title: String = "VisionForge page",

View File

@@ -9,7 +9,6 @@ import kotlin.test.Test
class TestServerExtensions {
@Suppress("UNUSED_VARIABLE")
@Test
fun testServerHeader(){
val string = createHTML().apply {