diff --git a/CHANGELOG.md b/CHANGELOG.md index 611e4c13..42282a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,14 @@ ### Added - Server module - Change collector +- Customizable accessors for colors ### Changed - Vision does not implement ItemProvider anymore. Property changes are done via `getProperty`/`setProperty` and `property` delegate. - Point3D and Point2D are made separate classes instead of expect/actual (to split up different engines. - JavaFX support moved to a separate module - Threejs support moved to a separate module -- \[Breaking change!\] Stylesheets are moved into properties under `@stylesheet` key +- \[Format breaking change!\] Stylesheets are moved into properties under `@stylesheet` key ### Deprecated diff --git a/build.gradle.kts b/build.gradle.kts index d4147448..4589fe5e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,8 +2,8 @@ plugins { id("ru.mipt.npm.project") } -val dataforgeVersion by extra("0.2.1-dev-4") -val ktorVersion by extra("1.4.3") +val dataforgeVersion by extra("0.3.0-dev") +val ktorVersion by extra("1.5.0") val htmlVersion by extra("0.7.2") val kotlinWrappersVersion by extra("pre.129-kotlin-1.4.20") val fxVersion by extra("14") diff --git a/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/GdmlFxDemoApp.kt b/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/GdmlFxDemoApp.kt index 1d5a4a89..03915243 100644 --- a/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/GdmlFxDemoApp.kt +++ b/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/GdmlFxDemoApp.kt @@ -2,6 +2,7 @@ package hep.dataforge.vision.gdml.demo import hep.dataforge.context.Global import hep.dataforge.vision.VisionManager +import hep.dataforge.vision.describedProperties import hep.dataforge.vision.editor.VisualObjectEditorFragment import hep.dataforge.vision.editor.VisualObjectTreeFragment import hep.dataforge.vision.gdml.toVision @@ -26,7 +27,7 @@ class GDMLView : View() { } private val propertyEditor = VisualObjectEditorFragment { - it.allProperties + it.describedProperties }.apply { descriptorProperty.set(SolidMaterial.descriptor) itemProperty.bind(treeFragment.selectedProperty) diff --git a/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/readFile.kt b/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/readFile.kt index da958c46..bcf18e51 100644 --- a/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/readFile.kt +++ b/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/readFile.kt @@ -1,7 +1,6 @@ package hep.dataforge.vision.gdml.demo import hep.dataforge.meta.DFExperimental -import hep.dataforge.meta.setItem import hep.dataforge.values.asValue import hep.dataforge.vision.gdml.readFile import hep.dataforge.vision.gdml.toVision diff --git a/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt b/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt index 73af1875..b4b883fc 100644 --- a/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt +++ b/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt @@ -1,7 +1,6 @@ package ru.mipt.npm.muon.monitor import hep.dataforge.vision.removeAll -import hep.dataforge.vision.setProperty import hep.dataforge.vision.solid.* import ru.mipt.npm.muon.monitor.Monitor.CENTRAL_LAYER_Z import ru.mipt.npm.muon.monitor.Monitor.LOWER_LAYER_Z @@ -53,7 +52,6 @@ class Model { detector(it) } } - tracks = group("tracks") } @@ -63,7 +61,6 @@ class Model { fun reset() { map.values.forEach { - it.config it.setProperty(SolidMaterial.MATERIAL_COLOR_KEY, null) } tracks.removeAll() diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt index 37220400..7e146287 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt @@ -9,8 +9,8 @@ import hep.dataforge.vision.Vision import hep.dataforge.vision.bootstrap.canvasControls import hep.dataforge.vision.bootstrap.card import hep.dataforge.vision.bootstrap.gridRow +import hep.dataforge.vision.bootstrap.visionPropertyEditor import hep.dataforge.vision.react.ThreeCanvasComponent -import hep.dataforge.vision.react.configEditor import hep.dataforge.vision.react.flexColumn import hep.dataforge.vision.react.objectTree import hep.dataforge.vision.solid.specifications.Camera @@ -57,7 +57,7 @@ val MMApp = functionalComponent("Muon monitor") { props -> val root = props.model.root - gridRow{ + gridRow { flexColumn { css { +"col-lg-3" @@ -109,8 +109,8 @@ val MMApp = functionalComponent("Muon monitor") { props -> height = 100.vh } styledDiv { - css{ - flex(0.0,1.0, FlexBasis.zero) + css { + flex(0.0, 1.0, FlexBasis.zero) } //settings canvas?.let { @@ -140,8 +140,8 @@ val MMApp = functionalComponent("Muon monitor") { props -> } } } - styledDiv{ - css{ + styledDiv { + css { padding(0.px) } nav { @@ -180,10 +180,10 @@ val MMApp = functionalComponent("Muon monitor") { props -> } } } - styledDiv{ - css{ - overflowY = Overflow.auto - } + styledDiv { + css { + overflowY = Overflow.auto + } //properties card("Properties") { selected.let { selected -> @@ -193,12 +193,7 @@ val MMApp = functionalComponent("Muon monitor") { props -> else -> root[selected] } if (selectedObject != null) { - configEditor( - selectedObject.config, - selectedObject.descriptor, - default = selectedObject.allProperties, - key = selected - ) + visionPropertyEditor(selectedObject, key = selected) } } } diff --git a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/geometry.kt b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/geometry.kt index c9c1512f..283d31ef 100644 --- a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/geometry.kt +++ b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/geometry.kt @@ -1,6 +1,9 @@ package ru.mipt.npm.sat +import hep.dataforge.meta.set import hep.dataforge.vision.solid.* +import hep.dataforge.vision.style +import hep.dataforge.vision.useStyle import kotlin.math.PI internal fun visionOfSatellite( @@ -12,7 +15,18 @@ internal fun visionOfSatellite( ySegmentSize: Number = xSegmentSize, fiberDiameter: Number = 1.0, ): SolidGroup = SolidGroup { - opacity = 0.3 + val transparent by style { + this[SolidMaterial.MATERIAL_OPACITY_KEY] = 0.3 + } + + val red by style { + this[SolidMaterial.MATERIAL_COLOR_KEY] = "red" + } + + val blue by style { + this[SolidMaterial.MATERIAL_COLOR_KEY] = "blue" + } + val totalXSize = xSegments * xSegmentSize.toDouble() val totalYSize = ySegments * ySegmentSize.toDouble() for (layer in 1..layers) { @@ -20,6 +34,7 @@ internal fun visionOfSatellite( for (i in 1..xSegments) { for (j in 1..ySegments) { box(xSegmentSize, ySegmentSize, layerHeight, name = "segment[$i,$j]") { + useStyle(transparent) z = (layer - 0.5) * layerHeight.toDouble() x = (i - 0.5) * xSegmentSize.toDouble() y = (j - 0.5) * ySegmentSize.toDouble() @@ -29,23 +44,21 @@ internal fun visionOfSatellite( group("fibers") { for (i in 1..xSegments) { cylinder(fiberDiameter, totalYSize) { + useStyle(red) rotationX = PI / 2 z = (layer - 1.0) * layerHeight.toDouble() + fiberDiameter.toDouble() x = (i - 0.5) * xSegmentSize.toDouble() - y = totalYSize/2 - - color("red") + y = totalYSize / 2 } } for (j in 1..ySegments) { cylinder(fiberDiameter, totalXSize) { + useStyle(blue) rotationY = PI / 2 z = (layer) * layerHeight.toDouble() - fiberDiameter.toDouble() y = (j - 0.5) * xSegmentSize.toDouble() - x = totalXSize/2 - - color("blue") + x = totalXSize / 2 } } } diff --git a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt index 685970bd..92b1b7d0 100644 --- a/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt +++ b/demo/sat-demo/src/main/kotlin/ru/mipt/npm/sat/satServer.kt @@ -1,11 +1,11 @@ package ru.mipt.npm.sat +import hep.dataforge.context.Global import hep.dataforge.names.toName -import hep.dataforge.vision.solid.Solid -import hep.dataforge.vision.solid.color -import hep.dataforge.vision.solid.invoke +import hep.dataforge.vision.solid.* import hep.dataforge.vision.three.server.* +import hep.dataforge.vision.visionManager import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -15,10 +15,14 @@ import kotlinx.html.h1 import kotlin.random.Random fun main() { + val satContext = Global.context("sat") { + plugin(SolidManager) + } + //Create a geometry val sat = visionOfSatellite(ySegments = 3) - val server = visionManager.serve { + val server = satContext.visionManager.serve { //use client library useThreeJs() //use css @@ -39,10 +43,11 @@ fun main() { val randomI = Random.nextInt(1, 4) val randomJ = Random.nextInt(1, 4) val target = "layer[$randomLayer].segment[$randomI,$randomJ]".toName() - (sat[target] as? Solid)?.color("red") - delay(300) - (sat[target] as? Solid)?.color("green") - delay(10) + val targetVision = sat[target] as Solid + targetVision.color("red") + delay(1000) + targetVision.color.clear() + delay(500) } } diff --git a/demo/spatial-showcase/src/commonMain/kotlin/hep/dataforge/vision/solid/demo/demo.kt b/demo/spatial-showcase/src/commonMain/kotlin/hep/dataforge/vision/solid/demo/demo.kt index 76c871e6..2dacf455 100644 --- a/demo/spatial-showcase/src/commonMain/kotlin/hep/dataforge/vision/solid/demo/demo.kt +++ b/demo/spatial-showcase/src/commonMain/kotlin/hep/dataforge/vision/solid/demo/demo.kt @@ -39,15 +39,18 @@ fun Page.showcase() { demo("shapes", "Basic shapes") { box(100.0, 100.0, 100.0) { z = -110.0 + color("teal") } sphere(50.0) { x = 110 detail = 16 + color("red") } tube(50, height = 10, innerRadius = 25, angle = PI) { y = 110 detail = 16 rotationX = PI / 4 + color("blue") } } @@ -55,6 +58,7 @@ fun Page.showcase() { val group = group { box(100, 100, 100) { z = 110.0 + opacity = 0.5 } box(100, 100, 100) { @@ -143,8 +147,7 @@ fun Page.showcaseCSG() { detail = 32 } material { - color(Colors.red) - wireframe = false + color(Colors.pink) } } composite(CompositeType.UNION) { @@ -154,8 +157,8 @@ fun Page.showcaseCSG() { sphere(50){ detail = 32 } - color(Colors.lightgreen) - opacity = 0.3 + color("lightgreen") + opacity = 0.7 } composite(CompositeType.SUBTRACT) { y = -300 @@ -165,7 +168,7 @@ fun Page.showcaseCSG() { sphere(50){ detail = 32 } - color(Colors.teal) + color("teal") opacity = 0.7 } } @@ -173,9 +176,11 @@ fun Page.showcaseCSG() { demo("CSG.custom", "CSG with manually created object") { intersect { tube(60, 10) { - detail = 64 + detail = 32 } box(100, 100, 100) + color("red") + opacity = 0.5 } } } \ No newline at end of file diff --git a/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/VariableBox.kt b/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/VariableBox.kt index ff610211..206f4f0c 100644 --- a/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/VariableBox.kt +++ b/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/VariableBox.kt @@ -9,9 +9,7 @@ import hep.dataforge.vision.set import hep.dataforge.vision.setProperty import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.Solid.Companion.GEOMETRY_KEY -import hep.dataforge.vision.solid.SolidMaterial.Companion.MATERIAL_COLOR_KEY import hep.dataforge.vision.solid.three.* -import hep.dataforge.vision.solid.three.ThreeMaterials.getMaterial import info.laht.threekt.core.BufferGeometry import info.laht.threekt.core.Object3D import info.laht.threekt.geometries.BoxBufferGeometry @@ -31,11 +29,9 @@ internal class VariableBox(xSize: Number, ySize: Number, zSize: Number) : ThreeV scaleX = xSize scaleY = ySize scaleZ = zSize - config[MeshThreeFactory.EDGES_ENABLED_KEY] = false - config[MeshThreeFactory.WIREFRAME_ENABLED_KEY] = false } - override fun render(): Object3D { + override fun render(three: ThreePlugin): Object3D { val xSize = getProperty(X_SIZE_KEY, false).number?.toDouble() ?: 1.0 val ySize = getProperty(Y_SIZE_KEY, false).number?.toDouble() ?: 1.0 val zSize = getProperty(Z_SIZE_KEY, false).number?.toDouble() ?: 1.0 @@ -44,9 +40,10 @@ internal class VariableBox(xSize: Number, ySize: Number, zSize: Number) : ThreeV //JS sometimes tries to pass Geometry as BufferGeometry @Suppress("USELESS_IS_CHECK") if (geometry !is BufferGeometry) error("BufferGeometry expected") - val mesh = Mesh(geometry, getMaterial(this@VariableBox, true)).apply { + val mesh = Mesh(geometry, ThreeMaterials.DEFAULT).apply { + updateMaterial(this@VariableBox) applyEdges(this@VariableBox) - applyWireFrame(this@VariableBox) + //applyWireFrame(this@VariableBox) //set position for mesh updatePosition(this@VariableBox) @@ -60,7 +57,7 @@ internal class VariableBox(xSize: Number, ySize: Number, zSize: Number) : ThreeV mesh.scale.set(xSize, ySize, zSize) //add listener to object properties - onPropertyChange(mesh) { name -> + onPropertyChange(three.context) { name -> when { name.startsWith(GEOMETRY_KEY) -> { val newXSize = getProperty(X_SIZE_KEY, false).number?.toDouble() ?: 1.0 @@ -69,14 +66,12 @@ internal class VariableBox(xSize: Number, ySize: Number, zSize: Number) : ThreeV mesh.scale.set(newXSize, newYSize, newZSize) mesh.updateMatrix() } - name.startsWith(MeshThreeFactory.WIREFRAME_KEY) -> mesh.applyWireFrame(this@VariableBox) name.startsWith(MeshThreeFactory.EDGES_KEY) -> mesh.applyEdges(this@VariableBox) - name.startsWith(MATERIAL_COLOR_KEY)->{ - mesh.material = getMaterial(this, true) - } + //name.startsWith(MATERIAL_COLOR_KEY) -> mesh.updateMaterialProperty(this, name) else -> mesh.updateProperty(this@VariableBox, name) } } + return mesh } @@ -100,7 +95,7 @@ internal class VariableBox(xSize: Number, ySize: Number, zSize: Number) : ThreeV color(r.toUByte(), g.toUByte(), b.toUByte()) } - companion object{ + companion object { private val X_SIZE_KEY = GEOMETRY_KEY + "xSize" private val Y_SIZE_KEY = GEOMETRY_KEY + "ySize" private val Z_SIZE_KEY = GEOMETRY_KEY + "zSize" diff --git a/playground/build.gradle.kts b/playground/build.gradle.kts index 45d20f6d..4df93ed9 100644 --- a/playground/build.gradle.kts +++ b/playground/build.gradle.kts @@ -12,6 +12,16 @@ repositories{ } kotlin { + + js(IR) { + browser { + webpackTask { + this.outputFileName = "js/visionforge-playground.js" + } + } + binaries.executable() + } + jvm{ compilations.all { kotlinOptions.jvmTarget = "11" @@ -20,30 +30,39 @@ kotlin { useJUnitPlatform() } } - js(IR) { - browser { + + afterEvaluate { + val jsBrowserDistribution by tasks.getting + + tasks.getByName("jvmProcessResources") { + dependsOn(jsBrowserDistribution) + afterEvaluate { + from(jsBrowserDistribution) + } } - binaries.executable() } + sourceSets { - commonMain { + val commonMain by getting { dependencies { implementation(project(":visionforge-solid")) implementation(project(":visionforge-gdml")) - + implementation(project(":visionforge-plotly")) } } + val jsMain by getting{ dependencies { implementation(project(":ui:bootstrap")) + implementation(project(":visionforge-threejs")) } } val jvmMain by getting{ dependencies { + implementation(project(":visionforge-server")) implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") - implementation(project(":visionforge-threejs:visionforge-threejs-server")) } } } diff --git a/playground/src/commonMain/kotlin/visionContext.kt b/playground/src/commonMain/kotlin/visionContext.kt new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/playground/src/commonMain/kotlin/visionContext.kt @@ -0,0 +1,2 @@ + + diff --git a/playground/src/jsMain/kotlin/PlayGroundApp.kt b/playground/src/jsMain/kotlin/PlayGroundApp.kt index a4635a04..4a3018a0 100644 --- a/playground/src/jsMain/kotlin/PlayGroundApp.kt +++ b/playground/src/jsMain/kotlin/PlayGroundApp.kt @@ -1,58 +1,67 @@ -import hep.dataforge.Application -import hep.dataforge.startApplication -import hep.dataforge.vision.bootstrap.visionPropertyEditor -import hep.dataforge.vision.react.ThreeCanvasComponent -import hep.dataforge.vision.react.objectTree -import hep.dataforge.vision.solid.* -import hep.dataforge.vision.solid.specifications.Canvas3DOptions -import kotlinx.browser.document -import org.w3c.dom.HTMLElement -import react.RBuilder -import react.child -import react.dom.div -import react.dom.render - -fun RBuilder.threeCanvas(object3D: Solid, options: Canvas3DOptions.() -> Unit = {}) { - child(ThreeCanvasComponent) { - attrs { - this.obj = object3D - this.options = Canvas3DOptions(options) - } - } -} - -private class PlayGroundApp : Application { - - override fun start(state: Map) { - - val element = - document.getElementById("app") as? HTMLElement ?: error("Element with id 'canvas' not found on page") - - val obj = SolidGroup().apply { - box(100, 100, 100, name = "A") - group("B") { - position = Point3D(120, 0, 0) - box(100, 100, 100, name = "C") - } - } - - render(element) { - div("row") { - div("col-3") { - objectTree(obj) - } - div("col-6") { - threeCanvas(obj) - } - div("col-3") { - visionPropertyEditor(obj) - } - } - } - } +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.meta.DFExperimental +import hep.dataforge.vision.client.VisionClient +import hep.dataforge.vision.client.renderAllVisions +import hep.dataforge.vision.plotly.PlotlyPlugin +import hep.dataforge.vision.solid.three.ThreePlugin +import kotlinx.browser.window +//fun RBuilder.threeCanvas(object3D: Solid, options: Canvas3DOptions.() -> Unit = {}) { +// child(ThreeCanvasComponent) { +// attrs { +// this.obj = object3D +// this.options = Canvas3DOptions(options) +// } +// } +//} +// +//private class PlayGroundApp : Application { +// +// override fun start(state: Map) { +// +// val element = +// document.getElementById("app") as? HTMLElement ?: error("Element with id 'canvas' not found on page") +// +// val obj = SolidGroup().apply { +// box(100, 100, 100, name = "A") +// group("B") { +// position = Point3D(120, 0, 0) +// box(100, 100, 100, name = "C") +// } +// } +// +// render(element) { +// div("row") { +// div("col-3") { +// objectTree(obj) +// } +// div("col-6") { +// threeCanvas(obj) +// } +// div("col-3") { +// visionPropertyEditor(obj) +// } +// } +// } +// } +// +//} + +public val visionContext: Context = Global.context("VISION") { + plugin(ThreePlugin) + plugin(PlotlyPlugin) + plugin(VisionClient) } +@DFExperimental fun main() { - startApplication(::PlayGroundApp) + //Loading three-js renderer + val clientManager = visionContext.plugins.fetch(VisionClient) + + //Fetch from server and render visions for all outputs + window.onload = { + clientManager.renderAllVisions() + } + //startApplication(::PlayGroundApp) } \ No newline at end of file diff --git a/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/randomSpheres.kt b/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/randomSpheres.kt new file mode 100644 index 00000000..728714af --- /dev/null +++ b/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/randomSpheres.kt @@ -0,0 +1,40 @@ +package hep.dataforge.vision.solid + +import hep.dataforge.meta.DFExperimental +import hep.dataforge.vision.ResourceLocation +import hep.dataforge.vision.VisionManager +import hep.dataforge.vision.html.fragment +import kotlinx.html.h1 +import java.nio.file.Paths +import kotlin.random.Random + +@OptIn(DFExperimental::class) +fun main() { + + val random = Random(112233) + val fragment = VisionManager.fragment { + h1 { +"Happy new year!" } + vision { + solid { + repeat(100) { + sphere(5, name = "sphere[$it]") { + x = random.nextDouble(-300.0, 300.0) + y = random.nextDouble(-300.0, 300.0) + z = random.nextDouble(-300.0, 300.0) + material { + color(random.nextInt()) + specularColor(random.nextInt()) + } + detail = 16 + } + } + } + } + } + + visionContext.makeVisionFile( + fragment, + Paths.get("randomSpheres.html"), + resourceLocation = ResourceLocation.EMBED + ) +} \ No newline at end of file diff --git a/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/serverExtensions.kt b/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/serverExtensions.kt new file mode 100644 index 00000000..c1f3b273 --- /dev/null +++ b/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/serverExtensions.kt @@ -0,0 +1,38 @@ +package hep.dataforge.vision.solid + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.meta.DFExperimental +import hep.dataforge.vision.ResourceLocation +import hep.dataforge.vision.VisionManager +import hep.dataforge.vision.html.HtmlVisionFragment +import hep.dataforge.vision.makeVisionFile +import hep.dataforge.vision.scriptHeader +import hep.dataforge.vision.three.server.VisionServer +import hep.dataforge.vision.three.server.useScript +import java.nio.file.Path + + +/** + * A global vision context used to resolve different vision renderers + */ +@DFExperimental +public val visionContext: Context = Global.context("VISION") { + plugin(VisionManager) + plugin(SolidManager) +} + +public fun VisionServer.usePlayground(): Unit { + useScript("js/visionforge-playground.js") +} + +@DFExperimental +public fun Context.makeVisionFile( + fragment: HtmlVisionFragment, + path: Path? = null, + title: String = "VisionForge page", + resourceLocation: ResourceLocation = ResourceLocation.SYSTEM, + show: Boolean = true, +): Unit = makeVisionFile(fragment, path = path, title = title, show = show) { actualPath -> + scriptHeader("js/visionforge-playground.js", actualPath, resourceLocation) +} diff --git a/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/simpleCube.kt b/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/simpleCube.kt index f4558c23..ca8e70b5 100644 --- a/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/simpleCube.kt +++ b/playground/src/jvmMain/kotlin/hep/dataforge/vision/solid/simpleCube.kt @@ -4,8 +4,6 @@ import hep.dataforge.meta.DFExperimental import hep.dataforge.vision.ResourceLocation import hep.dataforge.vision.VisionManager import hep.dataforge.vision.html.fragment -import hep.dataforge.vision.three.server.makeFile -import hep.dataforge.vision.three.server.solid @OptIn(DFExperimental::class) fun main() { @@ -17,5 +15,5 @@ fun main() { } } - fragment.makeFile(resourceLocation = ResourceLocation.LOCAL) + visionContext.makeVisionFile(fragment = fragment, resourceLocation = ResourceLocation.SYSTEM) } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 978abd5e..36931914 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { - val kotlinVersion = "1.4.20" - val toolsVersion = "0.7.0" + val kotlinVersion = "1.4.21" + val toolsVersion = "0.7.1" repositories { mavenLocal() @@ -33,7 +33,7 @@ rootProject.name = "visionforge" include( // ":ui", ":ui:react", - ":ui:ring", +// ":ui:ring", // ":ui:material", ":ui:bootstrap", ":visionforge-core", @@ -43,6 +43,7 @@ include( ":visionforge-threejs:visionforge-threejs-server", ":visionforge-gdml", ":visionforge-server", + ":visionforge-plotly", ":demo:spatial-showcase", ":demo:gdml", ":demo:muon-monitor", diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt index 68d90705..7c26abbf 100644 --- a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt @@ -51,11 +51,7 @@ public val ThreeControls: FunctionalComponent = functionalCo else -> (vision as? VisionGroup)?.get(selected) } if (selectedObject != null) { - visionPropertyEditor( - selectedObject, - default = selectedObject.allProperties, - key = selected - ) + visionPropertyEditor(selectedObject, key = selected) } } } @@ -69,7 +65,7 @@ public fun RBuilder.threeControls( canvas: ThreeCanvas, selected: Name?, onSelect: (Name) -> Unit = {}, - builder: TabBuilder.() -> Unit = {} + builder: TabBuilder.() -> Unit = {}, ): ReactElement = child(ThreeControls) { attrs { this.canvas = canvas diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/visionPropertyEditor.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/visionPropertyEditor.kt index 5bc10577..2d8b96f0 100644 --- a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/visionPropertyEditor.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/visionPropertyEditor.kt @@ -1,30 +1,38 @@ package hep.dataforge.vision.bootstrap -import hep.dataforge.meta.Meta import hep.dataforge.meta.descriptors.NodeDescriptor -import hep.dataforge.vision.Vision -import hep.dataforge.vision.getStyle -import hep.dataforge.vision.react.configEditor +import hep.dataforge.vision.* import hep.dataforge.vision.react.metaViewer +import hep.dataforge.vision.react.propertyEditor + import hep.dataforge.vision.solid.SolidReference import org.w3c.dom.Element import react.RBuilder import react.dom.render public fun RBuilder.visionPropertyEditor( - item: Vision, - descriptor: NodeDescriptor? = item.descriptor, - default: Meta? = null, - key: Any? = null + vision: Vision, + descriptor: NodeDescriptor? = vision.descriptor, + key: Any? = null, ) { + card("Properties") { - configEditor(item.config, descriptor, default, key) + propertyEditor( + provider = vision.ownProperties, + defaultProvider = vision.allProperties(), + updateFlow = vision.propertyChanges, + descriptor = descriptor, + key = key) } - val styles = item.styles - if(styles.isNotEmpty()) { + val styles = if (vision is SolidReference) { + (vision.styles + vision.prototype.styles).distinct() + } else { + vision.styles + } + if (styles.isNotEmpty()) { card("Styles") { accordion("styles") { styles.forEach { styleName -> - val style = item.getStyle(styleName) + val style = vision.getStyle(styleName) if (style != null) { entry(styleName) { metaViewer(style) @@ -39,7 +47,6 @@ public fun RBuilder.visionPropertyEditor( public fun Element.visionPropertyEditor( item: Vision, descriptor: NodeDescriptor? = item.descriptor, - default: Meta? = null ): Unit = render(this) { - visionPropertyEditor(item, descriptor, default) + visionPropertyEditor(item, descriptor = descriptor) } \ No newline at end of file diff --git a/ui/react/src/main/kotlin/hep/dataforge/vision/react/MetaViewer.kt b/ui/react/src/main/kotlin/hep/dataforge/vision/react/MetaViewer.kt index 9c17206d..b3b1b25f 100644 --- a/ui/react/src/main/kotlin/hep/dataforge/vision/react/MetaViewer.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vision/react/MetaViewer.kt @@ -1,7 +1,8 @@ package hep.dataforge.vision.react import hep.dataforge.meta.Meta -import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.NodeItem +import hep.dataforge.meta.ValueItem import hep.dataforge.meta.descriptors.ItemDescriptor import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.meta.descriptors.defaultItem @@ -51,7 +52,7 @@ private fun RBuilder.metaViewerItem(props: MetaViewerProps) { } when (actualItem) { - is MetaItem.NodeItem -> { + is NodeItem -> { styledDiv { css { +TreeStyles.treeLeaf @@ -108,7 +109,7 @@ private fun RBuilder.metaViewerItem(props: MetaViewerProps) { } } } - is MetaItem.ValueItem -> { + is ValueItem -> { styledDiv { css { +TreeStyles.treeLeaf diff --git a/ui/react/src/main/kotlin/hep/dataforge/vision/react/ConfigEditor.kt b/ui/react/src/main/kotlin/hep/dataforge/vision/react/PropertyEditor.kt similarity index 54% rename from ui/react/src/main/kotlin/hep/dataforge/vision/react/ConfigEditor.kt rename to ui/react/src/main/kotlin/hep/dataforge/vision/react/PropertyEditor.kt index a4ecce5e..bcc27f68 100644 --- a/ui/react/src/main/kotlin/hep/dataforge/vision/react/ConfigEditor.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vision/react/PropertyEditor.kt @@ -7,6 +7,16 @@ import hep.dataforge.names.NameToken import hep.dataforge.names.lastOrNull import hep.dataforge.names.plus import hep.dataforge.values.Value +import hep.dataforge.vision.hidden +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.html.js.onClickFunction import org.w3c.dom.Element import org.w3c.dom.events.Event @@ -14,55 +24,75 @@ import react.* import react.dom.render import styled.* -public external interface ConfigEditorItemProps : RProps { +public external interface PropertyEditorProps : RProps { /** * Root config object - always non null */ - public var root: Config + public var provider: MutableItemProvider /** - * Full path to the displayed node in [root]. Could be empty + * Provide default item (greyed out if used) */ - public var name: Name + public var defaultProvider: ItemProvider? /** - * Root default + * Full path to the displayed node in [provider]. Could be empty */ - public var default: Meta? + public var name: Name? /** * Root descriptor */ public var descriptor: NodeDescriptor? + + + /** + * A coroutine scope for updates + */ + public var scope: CoroutineScope? + + /** + * Flow names of updated properties + */ + public var updateFlow: Flow? } -private val ConfigEditorItem: FunctionalComponent = +private val PropertyEditorItem: FunctionalComponent = functionalComponent("ConfigEditorItem") { props -> - configEditorItem(props) + propertyEditorItem(props) } -private fun RBuilder.configEditorItem(props: ConfigEditorItemProps) { +private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) { var expanded: Boolean by useState { true } - var item: MetaItem? by useState { props.root[props.name] } - val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name) - val defaultItem = props.default?.get(props.name) - var actualItem: MetaItem? by useState { item ?: defaultItem ?: descriptorItem?.defaultItem() } + val itemName = props.name ?: Name.EMPTY + val descriptorItem: ItemDescriptor? = + useMemo({ props.descriptor?.get(itemName) }, arrayOf(props.descriptor, itemName)) - val token = props.name.lastOrNull()?.toString() ?: "Properties" + var item: MetaItem? by useState { props.provider.getItem(itemName) } + + if (descriptorItem?.hidden == true) return //fail fast for hidden property + + var actualItem: MetaItem? by useState { + item ?: props.defaultProvider?.getItem(itemName) ?: descriptorItem?.defaultItem() + } + + val token = itemName.lastOrNull()?.toString() ?: "Properties" fun update() { - item = props.root[props.name] - actualItem = item ?: defaultItem ?: descriptorItem?.defaultItem() + item = props.provider.getItem(itemName) + actualItem = item ?: props.defaultProvider?.getItem(itemName) ?: descriptorItem?.defaultItem() } - useEffectWithCleanup(listOf(props.root)) { - props.root.onChange(this) { name, _, _ -> - if (name == props.name) { - update() - } + if (props.updateFlow != null) { + useEffectWithCleanup(listOf(props.provider, props.updateFlow)) { + val updateJob = props.updateFlow!!.onEach { updatedName -> + if (updatedName == props.name) { + update() + } + }.launchIn(props.scope ?: GlobalScope) + return@useEffectWithCleanup { updateJob.cancel() } } - return@useEffectWithCleanup { props.root.removeListener(this) } } val expanderClick: (Event) -> Unit = { @@ -71,21 +101,19 @@ private fun RBuilder.configEditorItem(props: ConfigEditorItemProps) { val valueChanged: (Value?) -> Unit = { if (it == null) { - props.root.remove(props.name) + props.provider.remove(itemName) } else { - props.root[props.name] = it + props.provider[itemName] = it } update() } val removeClick: (Event) -> Unit = { - props.root.remove(props.name) + props.provider.remove(itemName) update() } - - - if (actualItem is MetaItem.NodeItem) { + if (actualItem is NodeItem) { styledDiv { css { +TreeStyles.treeLeaf @@ -121,7 +149,6 @@ private fun RBuilder.configEditorItem(props: ConfigEditorItemProps) { add(NameToken(it)) } item?.node?.items?.keys?.let { addAll(it) } - defaultItem?.node?.items?.keys?.let { addAll(it) } } keys.filter { !it.body.startsWith("@") }.forEach { token -> @@ -129,12 +156,11 @@ private fun RBuilder.configEditorItem(props: ConfigEditorItemProps) { css { +TreeStyles.treeItem } - child(ConfigEditorItem) { + child(PropertyEditorItem) { attrs { this.key = props.name.toString() - this.root = props.root - this.name = props.name + token - this.default = props.default + this.provider = props.provider + this.name = itemName + token this.descriptor = props.descriptor } } @@ -166,7 +192,7 @@ private fun RBuilder.configEditorItem(props: ConfigEditorItemProps) { +TreeStyles.resizeableInput } valueChooser( - props.name, + itemName, actualItem, descriptorItem as? ValueDescriptor, valueChanged @@ -190,63 +216,68 @@ private fun RBuilder.configEditorItem(props: ConfigEditorItemProps) { } } -public external interface ConfigEditorProps : RProps { - public var id: Name - public var root: Config - public var default: Meta? - public var descriptor: NodeDescriptor? -} @JsExport -public val ConfigEditor: FunctionalComponent = functionalComponent("ConfigEditor") { props -> - child(ConfigEditorItem) { +public val PropertyEditor: FunctionalComponent = functionalComponent("PropertyEditor") { props -> + child(PropertyEditorItem) { attrs { this.key = "" - this.root = props.root + this.provider = props.provider + this.defaultProvider = props.defaultProvider this.name = Name.EMPTY - this.default = props.default this.descriptor = props.descriptor + this.scope = props.scope } } } +public fun RBuilder.propertyEditor( + provider: MutableItemProvider, + defaultProvider: ItemProvider?, + updateFlow: Flow? = null, + descriptor: NodeDescriptor? = null, + scope: CoroutineScope? = null, + key: Any? = null, +) { + child(PropertyEditor) { + attrs { + this.provider = provider + this.defaultProvider = defaultProvider + this.updateFlow = updateFlow + this.descriptor = descriptor + this.key = key?.toString() ?: "" + this.scope = scope + } + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +private fun Config.flowUpdates(): Flow = callbackFlow { + onChange(this) { name, _, _ -> + launch { + send(name) + } + } + awaitClose { + removeListener(this) + } +} + + +public fun RBuilder.configEditor( + config: Config, + default: ItemProvider? = null, + descriptor: NodeDescriptor? = null, + key: Any? = null, + scope: CoroutineScope? = null, +): Unit = propertyEditor(config, default, config.flowUpdates(), descriptor, scope, key = key) + public fun Element.configEditor( config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null, key: Any? = null, -) { - render(this) { - child(ConfigEditor) { - attrs { - this.key = key?.toString() ?: "" - this.root = config - this.descriptor = descriptor - this.default = default - } - } - } -} - -public fun RBuilder.configEditor( - config: Config, - descriptor: NodeDescriptor? = null, - default: Meta? = null, - key: Any? = null, -) { - child(ConfigEditor) { - attrs { - this.key = key?.toString() ?: "" - this.root = config - this.descriptor = descriptor - this.default = default - } - } -} -// -//public fun RBuilder.configEditor( -// obj: Configurable, -// descriptor: NodeDescriptor?, -// default: Meta? = null, -// key: Any? = null -//): Unit = configEditor(obj.config, descriptor, default, key) + scope: CoroutineScope? = null, +): Unit = render(this) { + configEditor(config, default, descriptor, key, scope) +} \ No newline at end of file diff --git a/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt b/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt index aa78a0fb..877ce5bc 100644 --- a/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt @@ -37,8 +37,7 @@ public val ThreeCanvasComponent: FunctionalComponent = functio if (canvas == null) { val element = elementRef.current as? HTMLElement ?: error("Canvas element not found") val three: ThreePlugin = props.context.plugins.fetch(ThreePlugin) - val newCanvas: ThreeCanvas = - three.createCanvas(element, props.options ?: Canvas3DOptions.empty()) + val newCanvas: ThreeCanvas = three.createCanvas(element, props.options ?: Canvas3DOptions.empty()) props.canvasCallback?.invoke(newCanvas) canvas = newCanvas } diff --git a/ui/react/src/main/kotlin/hep/dataforge/vision/react/TreeStyles.kt b/ui/react/src/main/kotlin/hep/dataforge/vision/react/TreeStyles.kt index 5afcedac..16602699 100644 --- a/ui/react/src/main/kotlin/hep/dataforge/vision/react/TreeStyles.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vision/react/TreeStyles.kt @@ -17,7 +17,7 @@ public object TreeStyles : StyleSheet("treeStyles", true) { /** * Style the caret/arrow */ - public val treeCaret by css { + public val treeCaret: RuleSet by css { cursor = Cursor.pointer userSelect = UserSelect.none /* Create the caret/arrow with a unicode, and style it */ @@ -32,7 +32,7 @@ public object TreeStyles : StyleSheet("treeStyles", true) { /** * Rotate the caret/arrow icon when clicked on (using JavaScript) */ - val treeCaredDown by css { + public val treeCaredDown:RuleSet by css { before { content = "\u25B6".quoted color = Color.black @@ -42,7 +42,7 @@ public object TreeStyles : StyleSheet("treeStyles", true) { } } - val treeItem by css { + public val treeItem:RuleSet by css { alignItems = Align.center paddingLeft = 10.px borderLeftStyle = BorderStyle.dashed @@ -53,27 +53,27 @@ public object TreeStyles : StyleSheet("treeStyles", true) { borderBottomColor = Color.lightGray } - val treeLeaf by css { + public val treeLeaf:RuleSet by css { display = Display.flex flexDirection = FlexDirection.row flexWrap = FlexWrap.nowrap //alignItems = Align.center } - val treeLabel by css { + public val treeLabel:RuleSet by css { overflow = Overflow.hidden flex(flexGrow = 1.0, flexShrink = 1.0) } - val treeLabelInactive by css { + public val treeLabelInactive: RuleSet by css { color = Color.lightGray } - val treeLabelSelected by css { + public val treeLabelSelected:RuleSet by css { backgroundColor = Color.lightBlue } - val linkButton by css { + public val linkButton:RuleSet by css { backgroundColor = Color.white border = "none" padding(left = 4.pt, right = 4.pt, top = 0.pt, bottom = 0.pt) @@ -86,7 +86,7 @@ public object TreeStyles : StyleSheet("treeStyles", true) { } } - val removeButton by css { + public val removeButton:RuleSet by css { backgroundColor = Color.white borderStyle = BorderStyle.solid borderRadius = 2.px @@ -104,7 +104,7 @@ public object TreeStyles : StyleSheet("treeStyles", true) { } } - val resizeableInput by css { + public val resizeableInput: RuleSet by css { overflow = Overflow.hidden maxWidth = 120.pt flex(flexGrow = 2.0, flexShrink = 2.0, flexBasis = 60.pt) diff --git a/ui/react/src/main/kotlin/hep/dataforge/vision/react/valueChooser.kt b/ui/react/src/main/kotlin/hep/dataforge/vision/react/valueChooser.kt index e0888a46..86af2e4c 100644 --- a/ui/react/src/main/kotlin/hep/dataforge/vision/react/valueChooser.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vision/react/valueChooser.kt @@ -9,142 +9,166 @@ import hep.dataforge.vision.widgetType import kotlinx.html.InputType import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onKeyDownFunction -import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLSelectElement import org.w3c.dom.events.Event import react.* -import react.dom.defaultValue import react.dom.option import styled.styledInput import styled.styledSelect public external interface ValueChooserProps : RProps { - var item: MetaItem<*>? - var descriptor: ValueDescriptor? - var valueChanged: ((Value?) -> Unit)? -} - -public external interface ValueChooserState : RState { - var rawInput: Boolean? + public var item: MetaItem? + public var descriptor: ValueDescriptor? + public var valueChanged: ((Value?) -> Unit)? } @JsExport -class ValueChooserComponent(props: ValueChooserProps) : RComponent(props) { - private val element = createRef() - - private fun getValue(): Value? { - val element = element.current ?: return null//state.element ?: return null - return when (element) { - is HTMLInputElement -> if (element.type == "checkbox") { - if (element.checked) True else False - } else { - element.value.asValue() +public val StringValueChooser: FunctionalComponent = + functionalComponent("StringValueChooser") { props -> + var value by useState(props.item.string ?: "") + val keyDown: (Event) -> Unit = { event -> + if (event.type == "keydown" && event.asDynamic().key == "Enter") { + value = (event.target as HTMLInputElement).value + if(value!= props.item.string) { + props.valueChanged?.invoke(value.asValue()) + } + } + } + val handleChange: (Event) -> Unit = { + value = (it.target as HTMLInputElement).value + } + styledInput(type = InputType.text) { + attrs { + this.value = value + onKeyDownFunction = keyDown + onChangeFunction = handleChange } - is HTMLSelectElement -> element.value.asValue() - else -> error("Unknown event target: $element") } } - private val commit: (Event) -> Unit = { _ -> - props.valueChanged?.invoke(getValue()) - } - - private val keyDown: (Event) -> Unit = { event -> - if (event.type == "keydown" && event.asDynamic().key == "Enter") { - commit(event) +@JsExport +public val BooleanValueChooser: FunctionalComponent = + functionalComponent("BooleanValueChooser") { props -> + var checkedValue by useState(props.item.boolean ?: false) + val handleChange: (Event) -> Unit = { + val newValue = (it.target as HTMLInputElement).checked + checkedValue = newValue + props.valueChanged?.invoke(newValue.asValue()) + } + styledInput(type = InputType.checkBox) { + attrs { + this.attributes["indeterminate"] = (checkedValue == null).toString() + checked = checkedValue + onChangeFunction = handleChange + } } } - override fun shouldComponentUpdate( - nextProps: ValueChooserProps, - nextState: ValueChooserState - ): Boolean = nextProps.item !== props.item - - override fun componentDidUpdate(prevProps: ValueChooserProps, prevState: ValueChooserState, snapshot: Any) { - (element.current as? HTMLInputElement)?.let { element -> - if (element.type == "checkbox") { - element.defaultChecked = props.item?.boolean ?: false - } else { - element.defaultValue = props.item?.string ?: "" +@JsExport +public val NumberValueChooser: FunctionalComponent = + functionalComponent("NumberValueChooser") { props -> + var value by useState(props.item.string ?: "") + val keyDown: (Event) -> Unit = { event -> + if (event.type == "keydown" && event.asDynamic().key == "Enter") { + value = (event.target as HTMLInputElement).value + val number = value.toDoubleOrNull() + if (number == null) { + console.error("The input value $value is not a number") + } else { + props.valueChanged?.invoke(number.asValue()) + } + } + } + val handleChange: (Event) -> Unit = { + value = (it.target as HTMLInputElement).value + } + styledInput(type = InputType.number) { + attrs { + this.value = value + onKeyDownFunction = keyDown + onChangeFunction = handleChange + props.descriptor?.attributes?.get("step").string?.let { + step = it + } + props.descriptor?.attributes?.get("min").string?.let { + min = it + } + props.descriptor?.attributes?.get("max").string?.let { + max = it + } } - element.indeterminate = props.item == null } } - private fun RBuilder.stringInput() = styledInput(type = InputType.text) { - attrs { - this.defaultValue = props.item?.string ?: "" - onKeyDownFunction = keyDown +@JsExport +public val ComboValueChooser: FunctionalComponent = + functionalComponent("ComboValueChooser") { props -> + var selected by useState(props.item.string ?: "") + val handleChange: (Event) -> Unit = { + selected = (it.target as HTMLSelectElement).value + props.valueChanged?.invoke(selected.asValue()) + } + styledSelect { + props.descriptor?.allowedValues?.forEach { + option { + +it.string + } + } + attrs { + this.value = props.item?.string ?: "" + multiple = false + onChangeFunction = handleChange + } } - ref = element } - override fun RBuilder.render() { - val descriptor = props.descriptor - val type = descriptor?.type?.firstOrNull() - when { - state.rawInput == true -> stringInput() - descriptor?.widgetType == "color" -> styledInput(type = InputType.color) { - ref = element - attrs { - this.defaultValue = props.item?.value?.let { value -> - if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int) - else value.string - } ?: "#000000" - onChangeFunction = commit - } - } - type == ValueType.BOOLEAN -> { - styledInput(type = InputType.checkBox) { - ref = element - attrs { - defaultChecked = props.item?.boolean ?: false - onChangeFunction = commit - } - } - } - type == ValueType.NUMBER -> styledInput(type = InputType.number) { - ref = element - attrs { - descriptor.attributes["step"].string?.let { - step = it - } - descriptor.attributes["min"].string?.let { - min = it - } - descriptor.attributes["max"].string?.let { - max = it - } - defaultValue = props.item?.string ?: "" - onKeyDownFunction = keyDown - } - } - descriptor?.allowedValues?.isNotEmpty() ?: false -> styledSelect { - descriptor!!.allowedValues.forEach { - option { - +it.string - } - } - ref = element - attrs { - this.value = props.item?.string ?: "" - multiple = false - onChangeFunction = commit - } - } - else -> stringInput() +@JsExport +public val ColorValueChooser: FunctionalComponent = + functionalComponent("ColorValueChooser") { props -> + var value by useState( + props.item.value?.let { value -> + if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int) + else value.string + } ?: "#000000" + ) + val handleChange: (Event) -> Unit = { + value = (it.target as HTMLInputElement).value + props.valueChanged?.invoke(value.asValue()) } + styledInput(type = InputType.color) { + attrs { + this.value = value + onChangeFunction = handleChange + } + } + } + +@JsExport +public val ValueChooser: FunctionalComponent = functionalComponent("ValueChooser") { props -> + val rawInput by useState(false) + + val descriptor = props.descriptor + val type = descriptor?.type?.firstOrNull() + + when { + rawInput -> child(StringValueChooser, props) + descriptor?.widgetType == "color" -> child(ColorValueChooser, props) + type == ValueType.BOOLEAN -> child(BooleanValueChooser, props) + type == ValueType.NUMBER -> child(NumberValueChooser, props) + descriptor?.allowedValues?.isNotEmpty() ?: false -> child(ComboValueChooser, props) + //TODO handle lists + else -> child(StringValueChooser, props) } } internal fun RBuilder.valueChooser( name: Name, - item: MetaItem<*>?, + item: MetaItem?, descriptor: ValueDescriptor? = null, - callback: (Value?) -> Unit + callback: (Value?) -> Unit, ) { - child(ValueChooserComponent::class) { + child(ValueChooser) { attrs { key = name.toString() this.item = item diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Colors.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Colors.kt index 7dc41632..605f398c 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Colors.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Colors.kt @@ -1,8 +1,6 @@ package hep.dataforge.vision -import hep.dataforge.meta.MetaItem -import hep.dataforge.meta.get -import hep.dataforge.meta.number +import hep.dataforge.meta.* import hep.dataforge.values.ValueType import hep.dataforge.values.int import hep.dataforge.values.string @@ -192,9 +190,9 @@ public object Colors { /** * Convert color represented as Meta to string of format #rrggbb */ - fun fromMeta(item: MetaItem<*>): String { + fun fromMeta(item: MetaItem): String { return when (item) { - is MetaItem.NodeItem<*> -> { + is NodeItem -> { val node = item.node rgbToString( node[RED_KEY].number?.toByte()?.toUByte() ?: 0u, @@ -202,7 +200,7 @@ public object Colors { node[BLUE_KEY].number?.toByte()?.toUByte() ?: 0u ) } - is MetaItem.ValueItem -> { + is ValueItem -> { if (item.value.type == ValueType.NUMBER) { rgbToString(item.value.int) } else { diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/StyleReference.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/StyleReference.kt new file mode 100644 index 00000000..ac8da104 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/StyleReference.kt @@ -0,0 +1,33 @@ +package hep.dataforge.vision + +import hep.dataforge.meta.DFExperimental +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import kotlin.properties.ReadOnlyProperty + +/** + * A reference to a style defined in a specific container + */ +public class StyleReference(public val owner: VisionGroup, public val name: String) + +private tailrec fun styleIsDefined(vision: Vision, reference: StyleReference): Boolean = when { + reference.owner === vision -> true + vision.parent == null -> false + else -> styleIsDefined(vision.parent!!, reference) +} + +@VisionBuilder +public fun Vision.useStyle(reference: StyleReference) { + //check that style is defined in a parent + //check(styleIsDefined(this, reference)) { "Style reference does not belong to a Vision parent" } + useStyle(reference.name) +} + +@DFExperimental +@VisionBuilder +public fun VisionGroup.style(builder: MetaBuilder.() -> Unit): ReadOnlyProperty = + ReadOnlyProperty { _, property -> + val styleName = property.name + styleSheet.define(styleName, Meta(builder)) + StyleReference(this, styleName) + } \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/StyleSheet.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/StyleSheet.kt index 4d3ea1f3..80d5cdd2 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/StyleSheet.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/StyleSheet.kt @@ -5,13 +5,14 @@ import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.asName import hep.dataforge.names.plus +import kotlinx.coroutines.launch /** * A container for styles */ public inline class StyleSheet(private val owner: VisionGroup) { - private val styleNode get() = owner.properties?.get(STYLESHEET_KEY).node + private val styleNode get() = owner.getOwnProperty(STYLESHEET_KEY).node public val items: Map? get() = styleNode?.items?.mapValues { it.value.node ?: Meta.EMPTY } @@ -21,11 +22,7 @@ public inline class StyleSheet(private val owner: VisionGroup) { * Define a style without notifying owner */ public fun define(key: String, style: Meta?) { - if (style == null) { - styleNode?.remove(key) - } else { - owner.config[STYLESHEET_KEY + key] = style - } + owner.setProperty(STYLESHEET_KEY + key, style) } /** @@ -43,7 +40,7 @@ public inline class StyleSheet(private val owner: VisionGroup) { * Create and set a style */ public operator fun set(key: String, builder: MetaBuilder.() -> Unit) { - val newStyle = get(key)?.edit(builder) ?: Meta(builder) + val newStyle = get(key)?.builder()?.apply(builder) ?: Meta(builder) set(key, newStyle.seal()) } @@ -58,7 +55,9 @@ internal fun Vision.styleChanged(key: String, oldStyle: Meta?, newStyle: Meta?) val tokens: Collection = ((oldStyle?.items?.keys ?: emptySet()) + (newStyle?.items?.keys ?: emptySet())) .map { it.asName() } - tokens.forEach { parent?.propertyChanged(it) } + parent?.scope?.launch { + tokens.forEach { parent?.notifyPropertyChanged(it) } + } } if (this is VisionGroup) { for (obj in this) { @@ -68,6 +67,15 @@ internal fun Vision.styleChanged(key: String, oldStyle: Meta?, newStyle: Meta?) } +/** + * List of names of styles applied to this object. Order matters. Not inherited. + */ +public var Vision.styles: List + get() = getOwnProperty(Vision.STYLE_KEY)?.stringList ?: emptyList() + set(value) { + setProperty(Vision.STYLE_KEY, value) + } + /** * A stylesheet for this group and its descendants. Stylesheet is not applied directly, * but instead is just a repository for named configurations. @@ -78,7 +86,7 @@ public val VisionGroup.styleSheet: StyleSheet get() = StyleSheet(this) * Add style name to the list of styles to be resolved later. The style with given name does not necessary exist at the moment. */ public fun Vision.useStyle(name: String) { - styles = (properties[Vision.STYLE_KEY]?.stringList ?: emptyList()) + name + styles = (getOwnProperty(Vision.STYLE_KEY)?.stringList ?: emptyList()) + name } @@ -86,12 +94,12 @@ public fun Vision.useStyle(name: String) { * Find a style with given name for given [Vision]. The style is not necessary applied to this [Vision]. */ public tailrec fun Vision.getStyle(name: String): Meta? = - properties?.get(StyleSheet.STYLESHEET_KEY + name).node ?: parent?.getStyle(name) + getOwnProperty(StyleSheet.STYLESHEET_KEY + name).node ?: parent?.getStyle(name) /** * Resolve an item in all style layers */ -public fun Vision.getStyleItems(name: Name): Sequence> { +public fun Vision.getStyleItems(name: Name): Sequence { return styles.asSequence().map { getStyle(it) }.map { @@ -102,4 +110,6 @@ public fun Vision.getStyleItems(name: Name): Sequence> { /** * Collect all styles for this object in a single laminate */ -public val Vision.allStyles: Laminate get() = Laminate(styles.mapNotNull(::getStyle)) \ No newline at end of file +public val Vision.allStyles: Laminate get() = Laminate(styles.mapNotNull(::getStyle)) + + diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt index 058a04b3..4e3b65a1 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt @@ -1,22 +1,28 @@ package hep.dataforge.vision -import hep.dataforge.meta.* +import hep.dataforge.meta.DFExperimental +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.MutableItemProvider import hep.dataforge.meta.descriptors.Described import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.get import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.names.toName import hep.dataforge.provider.Type -import hep.dataforge.values.asValue import hep.dataforge.vision.Vision.Companion.TYPE -import hep.dataforge.vision.Vision.Companion.VISIBLE_KEY +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.serialization.Transient /** * A root type for display hierarchy */ @Type(TYPE) -public interface Vision : Configurable, Described { +public interface Vision : Described { /** * The parent object of this one. If null, this one is a root. @@ -25,46 +31,69 @@ public interface Vision : Configurable, Described { public var parent: VisionGroup? /** - * Nullable version of [config] used to check if this [Vision] has custom properties + * Properties belonging to this [Vision] potentially including artificial properties */ - public val properties: Config? + @Transient + public val meta: Meta /** - * All properties including styles and prototypes if present, including inherited ones + * A coroutine scope for asynchronous calls and locks */ - public val allProperties: Laminate + public val scope: CoroutineScope get() = parent?.scope ?: GlobalScope /** - * Get property (including styles). [inherit] toggles parent node property lookup + * A fast accessor method to get own property (no inheritance or styles). + * Should be equivalent to `getProperty(name,false,false,false)`. */ - public fun getProperty(name: Name, inherit: Boolean = true): MetaItem<*>? + public fun getOwnProperty(name: Name): MetaItem? /** - * Trigger property invalidation event. If [name] is empty, notify that the whole object is changed + * Get property. + * @param inherit toggles parent node property lookup. Null means inference from descriptor. Default is false. + * @param includeStyles toggles inclusion of. Null means inference from descriptor. Default is true. */ - public fun propertyChanged(name: Name): Unit + public fun getProperty( + name: Name, + inherit: Boolean = false, + includeStyles: Boolean = true, + includeDefaults: Boolean = true, + ): MetaItem? + /** - * Add listener triggering on property change + * Set the property value */ - public fun onPropertyChange(owner: Any?, action: (Name) -> Unit): Unit + public fun setProperty(name: Name, item: MetaItem?, notify: Boolean = true) /** - * Remove change listeners with given owner. + * Subscribe on property updates. The subscription is bound to the given [scope] and canceled when the scope is canceled */ - public fun removeChangeListener(owner: Any?) + public fun onPropertyChange(scope: CoroutineScope, callback: suspend (Name) -> Unit) /** - * List of names of styles applied to this object. Order matters. Not inherited. + * Flow of property invalidation events. It does not contain property values after invalidation since it is not clear + * if it should include inherited properties etc. */ - public var styles: List - get() = properties[STYLE_KEY]?.stringList ?: emptyList() - set(value) { - config[STYLE_KEY] = value + @DFExperimental + @OptIn(ExperimentalCoroutinesApi::class) + public val propertyChanges: Flow + get() = callbackFlow { + coroutineScope { + onPropertyChange(this) { + send(it) + } + awaitClose { cancel() } + } } + /** - * Update this vision using external meta. Children are not updated. + * Notify all listeners that a property has been changed and should be invalidated + */ + public suspend fun notifyPropertyChanged(propertyName: Name): Unit + + /** + * Update this vision using a dif represented by [VisionChange]. */ public fun update(change: VisionChange) @@ -78,60 +107,61 @@ public interface Vision : Configurable, Described { } } +public fun Vision.asyncNotifyPropertyChange(propertyName: Name) { + scope.launch { + notifyPropertyChanged(propertyName) + } +} + +/** + * Own properties, excluding inheritance, styles and descriptor + */ +public val Vision.ownProperties: MutableItemProvider + get() = object : MutableItemProvider { + override fun getItem(name: Name): MetaItem? = getOwnProperty(name) + override fun setItem(name: Name, item: MetaItem?): Unit = setProperty(name, item) + } + + +/** + * Convenient accessor for all properties of a vision. + * @param inherit - inherit property value from the parent by default. If null, inheritance is inferred from descriptor + */ +public fun Vision.allProperties( + inherit: Boolean? = null, + includeStyles: Boolean? = null, + includeDefaults: Boolean = true, +): MutableItemProvider = object : MutableItemProvider { + override fun getItem(name: Name): MetaItem? = getProperty( + name, + inherit = inherit ?: (descriptor?.get(name)?.inherited != false), + includeStyles = includeStyles ?: (descriptor?.get(name)?.usesStyles == true), + includeDefaults = includeDefaults + ) + + override fun setItem(name: Name, item: MetaItem?): Unit = setProperty(name, item) +} + /** * Get [Vision] property using key as a String */ -public fun Vision.getProperty(key: String, inherit: Boolean = true): MetaItem<*>? = - getProperty(key.toName(), inherit) +public fun Vision.getProperty( + key: String, + inherit: Boolean = false, + includeStyles: Boolean = true, + includeDefaults: Boolean = true, +): MetaItem? = getProperty(key.toName(), inherit, includeStyles, includeDefaults) /** * A convenience method to pair [getProperty] */ -public fun Vision.setProperty(key: Name, value: Any?) { - config[key] = value +public fun Vision.setProperty(key: Name, item: Any?) { + setProperty(key, MetaItem.of(item)) } /** * A convenience method to pair [getProperty] */ -public fun Vision.setProperty(key: String, value: Any?) { - config[key] = value +public fun Vision.setProperty(key: String, item: Any?) { + setProperty(key.toName(), MetaItem.of(item)) } - -/** - * Control visibility of the element - */ -public var Vision.visible: Boolean? - get() = getProperty(VISIBLE_KEY).boolean - set(value) = config.setValue(VISIBLE_KEY, value?.asValue()) - -///** -// * Convenience delegate for properties -// */ -//public fun Vision.property( -// default: MetaItem<*>? = null, -// key: Name? = null, -// inherit: Boolean = true, -//): MutableItemDelegate = -// object : ReadWriteProperty?> { -// override fun getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*>? { -// val name = key ?: property.name.toName() -// return getProperty(name, inherit) ?: default -// } -// -// override fun setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>?) { -// val name = key ?: property.name.toName() -// setProperty(name, value) -// } -// } - -public fun Vision.props(inherit: Boolean = true): MutableItemProvider = object : MutableItemProvider { - override fun getItem(name: Name): MetaItem<*>? { - return getProperty(name, inherit) - } - - override fun setItem(name: Name, item: MetaItem<*>?) { - setProperty(name, item) - } - -} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt index 6127479c..dddcb377 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt @@ -3,108 +3,134 @@ package hep.dataforge.vision import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.meta.descriptors.defaultItem -import hep.dataforge.meta.descriptors.defaultMeta import hep.dataforge.meta.descriptors.get import hep.dataforge.names.Name import hep.dataforge.names.asName +import hep.dataforge.names.plus +import hep.dataforge.values.Null import hep.dataforge.values.ValueType import hep.dataforge.vision.Vision.Companion.STYLE_KEY +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import kotlin.jvm.Synchronized internal data class PropertyListener( val owner: Any? = null, val action: (name: Name) -> Unit, ) +/** + * A full base implementation for a [Vision] + * @param properties Object own properties excluding styles and inheritance + */ @Serializable @SerialName("vision") -public open class VisionBase : Vision { +public open class VisionBase(internal var properties: Config? = null) : Vision { + + init { + //used during deserialization only + properties?.onChange(this) { name, oldItem, newItem -> + if (oldItem != newItem) { + scope.launch { + notifyPropertyChanged(name) + } + } + } + } @Transient override var parent: VisionGroup? = null - /** - * Object own properties excluding styles and inheritance - */ - override var properties: Config? = null - protected set + override val meta: Meta get() = properties ?: Meta.EMPTY - override val descriptor: NodeDescriptor? get() = null - - protected fun updateStyles(names: List) { - names.mapNotNull { getStyle(it) }.asSequence() - .flatMap { it.items.asSequence() } - .distinctBy { it.key } - .forEach { - propertyChanged(it.key.asName()) + @Synchronized + protected fun getOrCreateConfig(): Config { + if (properties == null) { + val newProperties = Config() + newProperties.onChange(this) { name, oldItem, newItem -> + if (oldItem != newItem) { + scope.launch { + notifyPropertyChanged(name) + } + } } + properties = newProperties + } + return properties!! } /** - * The config is initialized and assigned on-demand. - * To avoid unnecessary allocations, one should access [getAllProperties] via [getProperty] instead. + * A fast accessor method to get own property (no inheritance or styles */ - override val config: Config by lazy { - properties ?: Config().also { config -> - properties = config.also { - it.onChange(this) { name, _, _ -> propertyChanged(name) } - } + override fun getOwnProperty(name: Name): MetaItem? { + return properties?.getItem(name) + } + + override fun getProperty( + name: Name, + inherit: Boolean, + includeStyles: Boolean, + includeDefaults: Boolean, + ): MetaItem? = sequence { + yield(getOwnProperty(name)) + if (includeStyles) { + yieldAll(getStyleItems(name)) } - } - - @Transient - private val listeners = HashSet() - - override fun propertyChanged(name: Name) { - if (name == STYLE_KEY) { - updateStyles(properties?.get(STYLE_KEY)?.stringList ?: emptyList()) - } - for (listener in listeners) { - listener.action(name) - } - } - - override fun onPropertyChange(owner: Any?, action: (Name) -> Unit) { - listeners.add(PropertyListener(owner, action)) - } - - override fun removeChangeListener(owner: Any?) { - listeners.removeAll { owner == null || it.owner == owner } - } - - /** - * All available properties in a layered form - */ - override val allProperties: Laminate - get() = Laminate( - properties, - allStyles, - parent?.allProperties, - descriptor?.defaultMeta(), - ) - - override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = sequence { - yield(properties?.get(name)) - yieldAll(getStyleItems(name)) if (inherit) { - yield(parent?.getProperty(name, inherit)) + yield(parent?.getProperty(name, inherit, includeStyles, includeDefaults)) } yield(descriptor?.get(name)?.defaultItem()) }.merge() - /** - * Reset all properties to their default values - */ - public fun resetProperties() { - properties?.removeListener(this) - properties = null + override fun setProperty(name: Name, item: MetaItem?, notify: Boolean) { + getOrCreateConfig().setItem(name, item) + if (notify) { + scope.launch { + notifyPropertyChanged(name) + } + } + } + + override val descriptor: NodeDescriptor? get() = null + + private suspend fun updateStyles(names: List) { + names.mapNotNull { getStyle(it) }.asSequence() + .flatMap { it.items.asSequence() } + .distinctBy { it.key } + .forEach { + notifyPropertyChanged(it.key.asName()) + } + } + + + //TODO check memory consumption for the flow + @Transient + private val propertyInvalidationFlow: MutableSharedFlow = MutableSharedFlow() + + @DFExperimental + override val propertyChanges: Flow get() = propertyInvalidationFlow + + override fun onPropertyChange(scope: CoroutineScope, callback: suspend (Name) -> Unit) { + propertyInvalidationFlow.onEach(callback).launchIn(scope) + } + + override suspend fun notifyPropertyChanged(propertyName: Name) { + if (propertyName == STYLE_KEY) { + updateStyles(styles) + } + propertyInvalidationFlow.emit(propertyName) } override fun update(change: VisionChange) { - change.propertyChange[Name.EMPTY]?.let { - config.update(it) + change.properties?.let { + updateProperties(Name.EMPTY, it.asMetaItem()) } } @@ -115,6 +141,21 @@ public open class VisionBase : Vision { multiple = true } } + + public fun Vision.updateProperties(at: Name, item: MetaItem) { + when (item) { + is ValueItem -> { + if (item.value == Null) { + setProperty(at, null) + } else + setProperty(at, item) + } + is NodeItem -> item.node.items.forEach { (token, childItem) -> + updateProperties(at + token, childItem) + } + } + } + } } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionChange.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionChange.kt index ca7396a7..8d799ef8 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionChange.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionChange.kt @@ -3,10 +3,14 @@ package hep.dataforge.vision import hep.dataforge.meta.* import hep.dataforge.names.Name import hep.dataforge.names.plus +import hep.dataforge.values.Null import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.serialization.* +import kotlin.jvm.Synchronized import kotlin.time.Duration /** @@ -14,29 +18,42 @@ import kotlin.time.Duration */ public class VisionChangeBuilder : VisionContainerBuilder { - private val propertyChange = HashMap() - private val childrenChange = HashMap() + private var reset: Boolean = false + private var vision: Vision? = null + private val propertyChange = Config() + private val children: HashMap = HashMap() - public fun isEmpty(): Boolean = propertyChange.isEmpty() && childrenChange.isEmpty() + public fun isEmpty(): Boolean = propertyChange.isEmpty() && propertyChange.isEmpty() && children.isEmpty() - public fun propertyChanged(visionName: Name, propertyName: Name, item: MetaItem<*>?) { - propertyChange - .getOrPut(visionName) { Config() } - .setItem(propertyName, item) + @Synchronized + private fun getOrPutChild(visionName: Name): VisionChangeBuilder = + children.getOrPut(visionName) { VisionChangeBuilder() } + + public fun propertyChanged(visionName: Name, propertyName: Name, item: MetaItem?) { + if (visionName == Name.EMPTY) { + //Write property removal as [Null] + propertyChange[propertyName] = (item ?: Null.asMetaItem()) + } else { + getOrPutChild(visionName).propertyChanged(Name.EMPTY, propertyName, item) + } } override fun set(name: Name, child: Vision?) { - childrenChange[name] = child + getOrPutChild(name).apply { + vision = child + reset = vision == null + } } /** * Isolate collected changes by creating detached copies of given visions */ public fun isolate(manager: VisionManager): VisionChange = VisionChange( - propertyChange.mapValues { it.value.seal() }, - childrenChange.mapValues { it.value?.isolate(manager) } + reset, + vision?.isolate(manager), + if (propertyChange.isEmpty()) null else propertyChange.seal(), + if (children.isEmpty()) null else children.mapValues { it.value.isolate(manager) } ) - //TODO optimize isolation for visions without parents? } private fun Vision.isolate(manager: VisionManager): Vision { @@ -47,16 +64,11 @@ private fun Vision.isolate(manager: VisionManager): Vision { @Serializable public data class VisionChange( - val propertyChange: Map, - val childrenChange: Map, -) { - public fun isEmpty(): Boolean = propertyChange.isEmpty() && childrenChange.isEmpty() - - /** - * A shortcut to the top level property dif - */ - public val properties: Meta? get() = propertyChange[Name.EMPTY] -} + public val reset: Boolean = false, + public val vision: Vision? = null, + @Serializable(MetaSerializer::class) public val properties: Meta? = null, + public val children: Map? = null, +) public inline fun VisionChange(manager: VisionManager, block: VisionChangeBuilder.() -> Unit): VisionChange = VisionChangeBuilder().apply(block).isolate(manager) @@ -69,16 +81,9 @@ private fun CoroutineScope.collectChange( ) { //Collect properties change - source.config.onChange(this) { propertyName, oldItem, newItem -> - if (oldItem != newItem) { - launch { - collector().propertyChanged(name, propertyName, newItem) - } - } - } - - coroutineContext[Job]?.invokeOnCompletion { - source.config.removeListener(this) + source.onPropertyChange(this) { propertyName -> + val newItem = source.getProperty(propertyName, inherit = false, includeStyles = false, includeDefaults = false) + collector().propertyChanged(name, propertyName, newItem) } if (source is VisionGroup) { @@ -89,17 +94,12 @@ private fun CoroutineScope.collectChange( //Subscribe for structure change if (source is MutableVisionGroup) { - source.onStructureChange(this) { token, before, after -> - before?.removeChangeListener(this) - (before as? MutableVisionGroup)?.removeStructureChangeListener(this) + source.structureChanges.onEach { (token, _, after) -> if (after != null) { collectChange(name + token, after, collector) } collector()[name + token] = after - } - coroutineContext[Job]?.invokeOnCompletion { - source.removeStructureChangeListener(this) - } + }.launchIn(this) } } } @@ -111,17 +111,23 @@ public fun Vision.flowChanges( ): Flow = flow { var collector = VisionChangeBuilder() - manager.context.collectChange(Name.EMPTY, this@flowChanges) { collector } + coroutineScope { + collectChange(Name.EMPTY, this@flowChanges) { collector } - while (currentCoroutineContext().isActive) { - //Wait for changes to accumulate - delay(collectionDuration) - //Propagate updates only if something is changed - if (!collector.isEmpty()) { - //emit changes - emit(collector.isolate(manager)) - //Reset the collector - collector = VisionChangeBuilder() + //Send initial vision state + val initialChange = VisionChange(vision = isolate(manager)) + emit(initialChange) + + while (currentCoroutineContext().isActive) { + //Wait for changes to accumulate + delay(collectionDuration) + //Propagate updates only if something is changed + if (!collector.isEmpty()) { + //emit changes + emit(collector.isolate(manager)) + //Reset the collector + collector = VisionChangeBuilder() + } } } } \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroup.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroup.kt index fdfb9e7a..0f3967af 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroup.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroup.kt @@ -2,6 +2,7 @@ package hep.dataforge.vision import hep.dataforge.names.* import hep.dataforge.provider.Provider +import kotlinx.coroutines.flow.Flow public interface VisionContainer { public operator fun get(name: Name): V? @@ -44,16 +45,6 @@ public interface VisionGroup : Provider, Vision, VisionContainer { } } - /** - * A fix for serialization bug that writes all proper parents inside the tree after deserialization - */ - public fun attachChildren() { - children.values.forEach { - it.parent = this - (it as? VisionGroup)?.attachChildren() - } - } - public companion object { public const val STYLE_TARGET: String = "style" } @@ -75,19 +66,12 @@ public interface VisionContainerBuilder { */ public interface MutableVisionGroup : VisionGroup, VisionContainerBuilder { - /** - * Add listener for children structure change. - * @param owner the handler to properly remove listeners - * @param action First argument of the action is the name of changed child. Second argument is the new value of the object. - */ - public fun onStructureChange(owner: Any?, action: (token: NameToken, before: Vision?, after: Vision?) -> Unit) + public data class StructureChange(val token: NameToken, val before: Vision?, val after: Vision?) /** - * Remove children change listener + * Flow structure changes of this group. Unconsumed changes are discarded */ - public fun removeStructureChangeListener(owner: Any?) - -// public operator fun set(name: Name, child: Vision?) + public val structureChanges: Flow } public operator fun VisionContainer.get(str: String): V? = get(str.toName()) diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt index 996b0853..6cc2b97b 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt @@ -1,7 +1,9 @@ package hep.dataforge.vision -import hep.dataforge.meta.configure import hep.dataforge.names.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -9,71 +11,45 @@ import kotlinx.serialization.Transient /** * Abstract implementation of mutable group of [Vision] + * + * @param childrenInternal Internal mutable container for group children */ @Serializable @SerialName("vision.group") -public open class VisionGroupBase : VisionBase(), MutableVisionGroup { - - /** - * Internal mutable container for group children - * TODO made protected due to [https://github.com/Kotlin/kotlinx.serialization/issues/1200] - */ - @SerialName("children") - protected val childrenInternal: MutableMap = LinkedHashMap() +public open class VisionGroupBase( + @SerialName("children") internal val childrenInternal: MutableMap = LinkedHashMap(), +) : VisionBase(), MutableVisionGroup { /** * A map of top level named children */ override val children: Map get() = childrenInternal - override fun propertyChanged(name: Name) { - super.propertyChanged(name) - for (obj in this) { - obj.propertyChanged(name) + init { + childrenInternal.values.forEach { + it.parent = this } } - private data class StructureChangeListener( - val owner: Any?, - val callback: (token: NameToken, before: Vision?, after: Vision?) -> Unit, - ) + override suspend fun notifyPropertyChanged(propertyName: Name) { + super.notifyPropertyChanged(propertyName) + for (obj in this) { + obj.notifyPropertyChanged(propertyName) + } + } @Transient - private val structureChangeListeners = HashSet() + private val _structureChanges: MutableSharedFlow = MutableSharedFlow() - /** - * Add listener for children change - */ - override fun onStructureChange(owner: Any?, action: (token: NameToken, before: Vision?, after: Vision?) -> Unit) { - structureChangeListeners.add( - StructureChangeListener( - owner, - action - ) - ) - } - - /** - * Remove children change listener - */ - override fun removeStructureChangeListener(owner: Any?) { - structureChangeListeners.removeAll { owner == null || it.owner === owner } - } + override val structureChanges: SharedFlow get() = _structureChanges /** * Propagate children change event upwards */ - protected fun childrenChanged(name: NameToken, before: Vision?, after: Vision?) { - structureChangeListeners.forEach { it.callback(name, before, after) } - } - - /** - * Remove a child with given name token - */ - public fun removeChild(token: NameToken): Vision? { - val removed = childrenInternal.remove(token) - removed?.parent = null - return removed + private fun childrenChanged(name: NameToken, before: Vision?, after: Vision?) { + scope.launch { + _structureChanges.emit(MutableVisionGroup.StructureChange(name, before, after)) + } } /** @@ -91,12 +67,22 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { /** * Set parent for given child and attach it */ - private fun attachChild(token: NameToken, child: Vision) { - if (child.parent == null) { - child.parent = this - childrenInternal[token] = child - } else if (child.parent !== this) { - error("Can't reassign existing parent for $child") + private fun attachChild(token: NameToken, child: Vision?) { + val before = children[token] + when { + child == null -> { + childrenInternal.remove(token) + } + child.parent == null -> { + child.parent = this + childrenInternal[token] = child + } + child.parent !== this -> { + error("Can't reassign existing parent for $child") + } + } + if (before != child) { + childrenChanged(token, before, child) } } @@ -133,13 +119,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { } name.length == 1 -> { val token = name.tokens.first() - val before = children[token] - if (child == null) { - removeChild(token) - } else { - attachChild(token, child) - } - childrenChanged(token, before, child) + attachChild(token, child) } else -> { //TODO add safety check @@ -150,18 +130,12 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { } override fun update(change: VisionChange) { - //update stylesheet -// val changeStyleSheet = change.styleSheet -// if (changeStyleSheet != null) { -// styleSheet { -// update(changeStyleSheet) -// } -// } - change.propertyChange.forEach {(childName,configChange)-> - get(childName)?.configure(configChange) - } - change.childrenChange.forEach { (name, child) -> - set(name, child) + change.children?.forEach { (name, change) -> + when { + change.reset -> set(name, null) + change.vision != null -> set(name, change.vision) + else -> get(name)?.update(change) + } } super.update(change) } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt index eb34ed84..9a831799 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt @@ -30,16 +30,11 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) { serializersModule = this@VisionManager.serializersModule } - public fun decodeFromString(string: String): Vision = jsonFormat.decodeFromString(visionSerializer, string).also { - (it as? VisionGroup)?.attachChildren() - } + public fun decodeFromString(string: String): Vision = jsonFormat.decodeFromString(visionSerializer, string) public fun encodeToString(vision: Vision): String = jsonFormat.encodeToString(visionSerializer, vision) - public fun decodeFromJson(json: JsonElement): Vision = - jsonFormat.decodeFromJsonElement(visionSerializer, json).also { - (it as? VisionGroup)?.attachChildren() - } + public fun decodeFromJson(json: JsonElement): Vision = jsonFormat.decodeFromJsonElement(visionSerializer, json) public fun encodeToJsonElement(vision: Vision): JsonElement = jsonFormat.encodeToJsonElement(visionSerializer, vision) diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionPropertyContainer.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionPropertyContainer.kt new file mode 100644 index 00000000..a8b3ecad --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionPropertyContainer.kt @@ -0,0 +1,18 @@ +package hep.dataforge.vision + +import hep.dataforge.meta.MetaItem +import hep.dataforge.names.Name + +/** + * Property containers are used to create a symmetric behaviors for vision properties and style builders + */ +public interface VisionPropertyContainer { + public fun getProperty( + name: Name, + inherit: Boolean = false, + includeStyles: Boolean = true, + includeDefaults: Boolean = true, + ): MetaItem? + + public fun setProperty(name: Name, item: MetaItem?, notify: Boolean = true) +} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/VisionTagConsumer.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/VisionTagConsumer.kt index 101bef5b..5d0a7be7 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/VisionTagConsumer.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/VisionTagConsumer.kt @@ -73,7 +73,7 @@ public abstract class VisionTagConsumer( @OptIn(DFExperimental::class) public inline fun TagConsumer.vision( - name: String, + name: String = DEFAULT_VISION_NAME, visionProvider: VisionOutput.() -> Vision, ): T = vision(name.toName(), visionProvider) @@ -103,5 +103,7 @@ public abstract class VisionTagConsumer( 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 DEFAULT_VISION_NAME = "vision" } } \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/staticHtmlRender.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/staticHtmlRender.kt index 1ecbdb8a..5a275e82 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/staticHtmlRender.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/staticHtmlRender.kt @@ -18,9 +18,10 @@ public fun FlowContent.embedVisionFragment( val consumer = object : VisionTagConsumer(consumer, idPrefix) { override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) { script { + type = "text/json" attributes["class"] = OUTPUT_DATA_CLASS unsafe { - +manager.encodeToString(vision) + +"\n${manager.encodeToString(vision)}\n" } } } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/misc.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/misc.kt index 0e17c123..fb070582 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/misc.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/misc.kt @@ -1,33 +1,28 @@ package hep.dataforge.vision -import hep.dataforge.meta.Laminate -import hep.dataforge.meta.MetaItem -import hep.dataforge.meta.descriptors.NodeDescriptor -import hep.dataforge.meta.node -import hep.dataforge.names.Name -import hep.dataforge.values.ValueType +import hep.dataforge.meta.* import hep.dataforge.values.asValue @DslMarker public annotation class VisionBuilder - -public fun Sequence?>.merge(): MetaItem<*>? { - return when (val first = firstOrNull { it != null }) { - null -> null - is MetaItem.ValueItem -> first //fast search for first entry if it is value - is MetaItem.NodeItem -> { - //merge nodes if first encountered node is meta - val laminate: Laminate = Laminate(mapNotNull { it.node }.toList()) - MetaItem.NodeItem(laminate) - } +public fun Sequence.merge(): MetaItem? = when (val first = firstOrNull { it != null }) { + null -> null + is ValueItem -> first //fast search for first entry if it is value + is NodeItem -> { + //merge nodes if first encountered node is meta + val laminate: Laminate = Laminate(mapNotNull { it.node }.toList()) + NodeItem(laminate) } } -public inline fun > NodeDescriptor.enum(key: Name, default: E?): Unit = value(key) { - type(ValueType.STRING) - default?.let { - default(default) - } - allowedValues = enumValues().map { it.asValue() } -} \ No newline at end of file +/** + * Control visibility of the element + */ +public var Vision.visible: Boolean? + get() = getProperty(Vision.VISIBLE_KEY).boolean + set(value) = setProperty(Vision.VISIBLE_KEY, value?.asValue()) + +public fun Vision.configure(meta: Meta?): Unit = update(VisionChange(properties = meta)) + +public fun Vision.configure(block: MetaBuilder.() -> Unit): Unit = configure(Meta(block)) \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/valueWidget.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/valueWidget.kt deleted file mode 100644 index f563d87c..00000000 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/valueWidget.kt +++ /dev/null @@ -1,27 +0,0 @@ -package hep.dataforge.vision - -import hep.dataforge.meta.* -import hep.dataforge.meta.descriptors.ValueDescriptor -import hep.dataforge.meta.descriptors.attributes - -/** - * Extension property to access the "widget" key of [ValueDescriptor] - */ -public var ValueDescriptor.widget: Meta - get() = attributes["widget"].node ?: Meta.EMPTY - set(value) { - attributes { - set("widget", value) - } - } - -/** - * Extension property to access the "widget.type" key of [ValueDescriptor] - */ -public var ValueDescriptor.widgetType: String? - get() = attributes["widget.type"].string - set(value) { - attributes{ - set("widget.type", value) - } - } \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionDescriptor.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionDescriptor.kt new file mode 100644 index 00000000..59001d15 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionDescriptor.kt @@ -0,0 +1,79 @@ +package hep.dataforge.vision + +import hep.dataforge.meta.* +import hep.dataforge.meta.descriptors.ItemDescriptor +import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.ValueDescriptor +import hep.dataforge.meta.descriptors.attributes +import hep.dataforge.names.Name +import hep.dataforge.values.ValueType +import hep.dataforge.values.asValue + +private const val INHERITED_DESCRIPTOR_ATTRIBUTE = "inherited" +private const val STYLE_DESCRIPTOR_ATTRIBUTE = "useStyles" + +public var ItemDescriptor.inherited: Boolean + get() = attributes[INHERITED_DESCRIPTOR_ATTRIBUTE].boolean ?: false + set(value) = attributes { + set(INHERITED_DESCRIPTOR_ATTRIBUTE, value) + } + +public var ItemDescriptor.usesStyles: Boolean + get() = attributes[STYLE_DESCRIPTOR_ATTRIBUTE].boolean ?: true + set(value) = attributes { + set(STYLE_DESCRIPTOR_ATTRIBUTE, value) + } + + +public val Vision.describedProperties: Meta + get() = Meta { + descriptor?.items?.forEach { (key, descriptor) -> + key put getProperty(key, inherit = descriptor.inherited) + } + } + +/** + * Extension property to access the "widget" key of [ValueDescriptor] + */ +public var ValueDescriptor.widget: Meta + get() = attributes["widget"].node ?: Meta.EMPTY + set(value) { + attributes { + set("widget", value) + } + } + +/** + * Extension property to access the "widget.type" key of [ValueDescriptor] + */ +public var ValueDescriptor.widgetType: String? + get() = attributes["widget.type"].string + set(value) { + attributes { + set("widget.type", value) + } + } + +/** + * If true, this item is hidden in property editor. Default is false + */ +public val ItemDescriptor.hidden: Boolean + get() = attributes["widget.hide"].boolean ?: false + +public fun ItemDescriptor.hide(): Unit = attributes { + set("widget.hide", true) +} + + +public inline fun > NodeDescriptor.enum( + key: Name, + default: E?, + crossinline modifier: ValueDescriptor.() -> Unit = {}, +): Unit = value(key) { + type(ValueType.STRING) + default?.let { + default(default) + } + allowedValues = enumValues().map { it.asValue() } + modifier() +} \ No newline at end of file diff --git a/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt b/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt index 0d40205a..2d2ad125 100644 --- a/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt +++ b/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt @@ -1,9 +1,9 @@ package hep.dataforge.vision.html import hep.dataforge.meta.DFExperimental -import hep.dataforge.meta.configure import hep.dataforge.meta.set import hep.dataforge.vision.VisionBase +import hep.dataforge.vision.configure import kotlinx.html.* import kotlinx.html.stream.createHTML import kotlin.test.Test @@ -35,7 +35,7 @@ class HtmlTagTest { div { h2 { +"Properties" } ul { - vision.properties?.items?.forEach { + (vision as? VisionBase)?.meta?.items?.forEach { li { a { +it.key.toString() } p { +it.value.toString() } diff --git a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt index 8dda1ebf..edd5ae4d 100644 --- a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt @@ -18,6 +18,7 @@ import org.w3c.dom.WebSocket import org.w3c.dom.asList import org.w3c.dom.get import org.w3c.dom.url.URL +import kotlin.collections.set import kotlin.reflect.KClass public class VisionClient : AbstractPlugin() { @@ -80,14 +81,18 @@ public class VisionClient : AbstractPlugin() { renderVision(element, embeddedVision, outputMeta) } - val endpoint = resolveEndpoint(element) - logger.info { "Vision server is resolved to $endpoint" } + element.attributes[OUTPUT_FETCH_ATTRIBUTE]?.let { attr -> - element.attributes[OUTPUT_FETCH_ATTRIBUTE]?.let { - - val fetchUrl = URL(endpoint).apply { + val fetchUrl = if (attr.value.isBlank() || attr.value == "auto") { + val endpoint = resolveEndpoint(element) + logger.info { "Vision server is resolved to $endpoint" } + URL(endpoint).apply { + pathname += "/vision" + } + } else { + URL(attr.value) + }.apply { searchParams.append("name", name) - pathname += "/vision" } logger.info { "Fetching vision data from $fetchUrl" } @@ -98,16 +103,22 @@ public class VisionClient : AbstractPlugin() { renderVision(element, vision, outputMeta) } } else { - logger.error { "Failed to fetch initial vision state from $endpoint" } + logger.error { "Failed to fetch initial vision state from $fetchUrl" } } } } - element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { - - val wsUrl = URL(endpoint).apply { - pathname += "/ws" + element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr -> + val wsUrl = if (attr.value.isBlank() || attr.value == "auto") { + val endpoint = resolveEndpoint(element) + logger.info { "Vision server is resolved to $endpoint" } + URL(endpoint).apply { + pathname += "/ws" + } + } else { + URL(attr.value) + }.apply { protocol = "ws" searchParams.append("name", name) } @@ -118,13 +129,18 @@ public class VisionClient : AbstractPlugin() { onmessage = { messageEvent -> val stringData: String? = messageEvent.data as? String if (stringData != null) { - val dif = visionManager.jsonFormat.decodeFromString( + val change = visionManager.jsonFormat.decodeFromString( VisionChange.serializer(), stringData ) - logger.debug { "Got update $dif for output with name $name" } - visionMap[element]?.update(dif) - ?: logger.info { "Target vision for element $element with name $name not found" } + + if (change.vision != null) { + renderVision(element, change.vision, outputMeta) + } + + logger.debug { "Got update $change for output with name $name" } + visionMap[element]?.update(change) + ?: console.info("Target vision for element $element with name $name not found") } else { console.error("WebSocket message data is not a string") } diff --git a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/headers.kt b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/headers.kt index 80e5b250..5dfb5352 100644 --- a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/headers.kt +++ b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/headers.kt @@ -35,30 +35,30 @@ public enum class ResourceLocation { EMBED } -internal const val DATAFORGE_ASSETS_PATH = ".dataforge/assets" +internal const val VISIONFORGE_ASSETS_PATH = ".dataforge/vision/assets" /** * Check if the asset exists in given local location and put it there if it does not * @param */ -internal fun checkOrStoreFile(basePath: Path, filePath: Path, resource: String): Path { - val fullPath = basePath.resolveSibling(filePath).toAbsolutePath() +internal fun checkOrStoreFile(htmlPath: Path, filePath: Path, resource: String): Path { + val fullPath = htmlPath.resolveSibling(filePath).toAbsolutePath().resolve(resource) if (Files.exists(fullPath)) { //TODO checksum } else { //TODO add logging - val bytes = VisionManager::class.java.getResourceAsStream(resource).readAllBytes() + val bytes = VisionManager::class.java.getResourceAsStream("/$resource").readAllBytes() Files.createDirectories(fullPath.parent) Files.write(fullPath, bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) } - return if (basePath.isAbsolute && fullPath.startsWith(basePath)) { - basePath.relativize(fullPath) + return if (htmlPath.isAbsolute && fullPath.startsWith(htmlPath.parent)) { + htmlPath.parent.relativize(fullPath) } else { - filePath + fullPath } } @@ -78,7 +78,7 @@ internal fun embedScriptHeader(resource: String): HtmlFragment = { script { type = "text/javascript" unsafe { - val bytes = VisionManager::class.java.getResourceAsStream(resource).readAllBytes() + val bytes = VisionManager::class.java.getResourceAsStream("/$resource").readAllBytes() +bytes.toString(Charsets.UTF_8) } } @@ -100,20 +100,20 @@ internal fun fileCssHeader( * Make a script header, automatically copying file to appropriate location */ @DFExperimental -public fun Context.Companion.scriptHeader( +public fun Context.scriptHeader( scriptResource: String, - basePath: Path, + htmlPath: Path, resourceLocation: ResourceLocation, ): HtmlFragment { val targetPath = when (resourceLocation) { ResourceLocation.LOCAL -> checkOrStoreFile( - basePath, - Path.of(DATAFORGE_ASSETS_PATH), + htmlPath, + Path.of(VISIONFORGE_ASSETS_PATH), scriptResource ) ResourceLocation.SYSTEM -> checkOrStoreFile( Path.of("."), - Path.of(System.getProperty("user.home")).resolve(DATAFORGE_ASSETS_PATH), + Path.of(System.getProperty("user.home")).resolve(VISIONFORGE_ASSETS_PATH), scriptResource ) ResourceLocation.EMBED -> null diff --git a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/htmlExport.kt b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/htmlExport.kt index f7539a52..d560f46a 100644 --- a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/htmlExport.kt +++ b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/htmlExport.kt @@ -1,9 +1,16 @@ package hep.dataforge.vision +import hep.dataforge.context.Context import hep.dataforge.meta.DFExperimental -import hep.dataforge.vision.html.* -import kotlinx.html.* +import hep.dataforge.vision.html.HtmlFragment +import hep.dataforge.vision.html.HtmlVisionFragment +import hep.dataforge.vision.html.embedVisionFragment +import hep.dataforge.vision.html.fragment +import kotlinx.html.body +import kotlinx.html.head +import kotlinx.html.meta import kotlinx.html.stream.createHTML +import kotlinx.html.title import java.awt.Desktop import java.nio.file.Files import java.nio.file.Path @@ -13,27 +20,27 @@ import java.nio.file.Path * Make a file with the embedded vision data */ @DFExperimental -public fun HtmlVisionFragment.makeFile( - manager: VisionManager, - vararg headers: HtmlFragment, +public fun Context.makeVisionFile( + fragment: HtmlVisionFragment, path: Path? = null, title: String = "VisionForge page", show: Boolean = true, + headerBuilder: (Path) -> HtmlFragment, ) { - val actualFile = path ?: Files.createTempFile("tempPlot", ".html") - Files.createDirectories(actualFile.parent) + val actualFile = path?.let { + Path.of(System.getProperty("user.home")).resolve(path) + } ?: Files.createTempFile("tempPlot", ".html") + //Files.createDirectories(actualFile.parent) val htmlString = createHTML().apply { head { meta { charset = "utf-8" - headers.forEach { - fragment(it) - } + fragment(headerBuilder(actualFile)) } title(title) } body { - embedVisionFragment(manager, fragment = this@makeFile) + embedVisionFragment(visionManager, fragment = fragment) } }.finalize() diff --git a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/editor/FXMeta.kt b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/editor/FXMeta.kt index 84a286ed..c7460ff3 100644 --- a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/editor/FXMeta.kt +++ b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/editor/FXMeta.kt @@ -18,7 +18,7 @@ import tornadofx.* /** * A display for meta and descriptor */ -sealed class FXMeta> : Comparable> { +sealed class FXMeta> : Comparable> { abstract val name: NameToken abstract val parent: FXMetaNode? abstract val descriptionProperty: ObservableStringValue @@ -35,7 +35,7 @@ sealed class FXMeta> : Comparable> { } companion object { - fun > root( + fun > root( node: M, descriptor: NodeDescriptor? = null, rootName: String = "root" @@ -47,7 +47,7 @@ sealed class FXMeta> : Comparable> { } } -class FXMetaNode>( +class FXMetaNode>( override val name: NameToken, override val parent: FXMetaNode?, nodeValue: M? = null, @@ -89,7 +89,7 @@ class FXMetaNode>( init { bind(nodeProperty, descriptorProperty) - val listener: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit = { name, _, _ -> + val listener: (Name, MetaItem?, MetaItem?) -> Unit = { name, _, _ -> if (name.length == 1) invalidate() } @@ -115,7 +115,7 @@ class FXMetaNode>( val actualItem = node?.items?.get(token) val actualDescriptor = descriptor?.items?.get(token.body) - if (actualItem is MetaItem.NodeItem || actualDescriptor is NodeDescriptor) { + if (actualItem is NodeItem || actualDescriptor is NodeDescriptor) { FXMetaNode(token, this@FXMetaNode) } else { FXMetaValue(token, this@FXMetaNode) @@ -134,7 +134,7 @@ class FXMetaNode>( } } -public class FXMetaValue>( +public class FXMetaValue>( override val name: NameToken, override val parent: FXMetaNode ) : FXMeta() { @@ -151,10 +151,10 @@ public class FXMetaValue>( //private val innerValueProperty = SimpleObjectProperty(value) public val valueProperty = descriptorProperty.objectBinding { descriptor -> - parent.node[name].value ?: descriptor?.default + parent.node?.get(name).value ?: descriptor?.default } - override val hasValue: ObservableBooleanValue = parent.nodeProperty.booleanBinding { it[name] != null } + override val hasValue: ObservableBooleanValue = parent.nodeProperty.booleanBinding { it?.get(name) != null } public val value by valueProperty @@ -169,12 +169,12 @@ public fun > FXMetaNode.remove(name: NameToken) { private fun > M.createEmptyNode(token: NameToken, append: Boolean): M { return if (append && token.hasIndex()) { val name = token.asName() - val index = (getIndexed(name).keys.mapNotNull { it.toIntOrNull() }.max() ?: -1) + 1 + val index = (getIndexed(name).keys.mapNotNull { it?.toIntOrNull() }.maxOrNull() ?: -1) + 1 val newName = name.withIndex(index.toString()) set(newName, Meta.EMPTY) get(newName).node!! } else { - this.setNode(token.asName(), Meta.EMPTY) + this.set(token.asName(), Meta.EMPTY) //FIXME possible concurrency bug get(token).node!! } diff --git a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/editor/VisualObjectEditorFragment.kt b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/editor/VisualObjectEditorFragment.kt index 251130c2..762c8090 100644 --- a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/editor/VisualObjectEditorFragment.kt +++ b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/editor/VisualObjectEditorFragment.kt @@ -1,12 +1,8 @@ package hep.dataforge.vision.editor -import hep.dataforge.meta.Config -import hep.dataforge.meta.Meta +import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.NodeDescriptor -import hep.dataforge.meta.update -import hep.dataforge.vision.Vision -import hep.dataforge.vision.getStyle -import hep.dataforge.vision.setProperty +import hep.dataforge.vision.* import javafx.beans.binding.Binding import javafx.beans.property.SimpleObjectProperty import javafx.scene.Node @@ -23,8 +19,8 @@ class VisualObjectEditorFragment(val selector: (Vision) -> Meta) : Fragment() { constructor( item: Vision?, descriptor: NodeDescriptor?, - selector: (Vision) -> Config = { it.config } - ) : this(selector) { + selector: (Vision) -> MutableItemProvider = { it.allProperties() }, + ) : this({ it.describedProperties }) { this.item = item this.descriptorProperty.set(descriptor) } diff --git a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FX3DPlugin.kt b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FX3DPlugin.kt index 3f863a34..35cd6806 100644 --- a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FX3DPlugin.kt +++ b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FX3DPlugin.kt @@ -43,9 +43,9 @@ class FX3DPlugin : AbstractPlugin() { } fun buildNode(obj: Solid): Node { - val binding = VisualObjectFXBinding(obj) + val binding = VisualObjectFXBinding(this, obj) return when (obj) { - is SolidReference -> referenceFactory(obj, binding) + is SolidReferenceGroup -> referenceFactory(obj, binding) is SolidGroup -> { Group(obj.children.mapNotNull { (token, obj) -> (obj as? Solid)?.let { @@ -71,7 +71,7 @@ class FX3DPlugin : AbstractPlugin() { is PolyLine -> PolyLine3D( obj.points.map { Point3D(it.x, it.y, it.z) }, obj.thickness.toFloat(), - obj.getProperty(SolidMaterial.MATERIAL_COLOR_KEY)?.color() + obj.getProperty(SolidMaterial.MATERIAL_COLOR_KEY, inherit = true)?.color() ).apply { this.meshView.cullFace = CullFace.FRONT } @@ -130,7 +130,7 @@ class FX3DPlugin : AbstractPlugin() { } companion object : PluginFactory { - override val tag = PluginTag("visual.fx3D", PluginTag.DATAFORGE_GROUP) + override val tag = PluginTag("vision.fx3D", PluginTag.DATAFORGE_GROUP) override val type = FX3DPlugin::class override fun invoke(meta: Meta, context: Context) = FX3DPlugin() } diff --git a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FXMaterials.kt b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FXMaterials.kt index 9ae8fcf3..9e417438 100644 --- a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FXMaterials.kt +++ b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FXMaterials.kt @@ -1,9 +1,6 @@ package hep.dataforge.vision.solid -import hep.dataforge.meta.MetaItem -import hep.dataforge.meta.double -import hep.dataforge.meta.get -import hep.dataforge.meta.int +import hep.dataforge.meta.* import hep.dataforge.values.ValueType import hep.dataforge.values.int import hep.dataforge.values.string @@ -35,9 +32,9 @@ public object FXMaterials { * Infer color based on meta item * @param opacity default opacity */ -public fun MetaItem<*>.color(opacity: Double = 1.0): Color { +public fun MetaItem.color(opacity: Double = 1.0): Color { return when (this) { - is MetaItem.ValueItem -> if (this.value.type == ValueType.NUMBER) { + is ValueItem -> if (this.value.type == ValueType.NUMBER) { val int = value.int val red = int and 0x00ff0000 shr 16 val green = int and 0x0000ff00 shr 8 @@ -46,7 +43,7 @@ public fun MetaItem<*>.color(opacity: Double = 1.0): Color { } else { Color.web(this.value.string) } - is MetaItem.NodeItem -> { + is NodeItem -> { Color.rgb( node[Colors.RED_KEY]?.int ?: 0, node[Colors.GREEN_KEY]?.int ?: 0, @@ -60,11 +57,11 @@ public fun MetaItem<*>.color(opacity: Double = 1.0): Color { /** * Infer FX material based on meta item */ -public fun MetaItem<*>?.material(): Material { +public fun MetaItem?.material(): Material { return when (this) { null -> FXMaterials.GREY - is MetaItem.ValueItem -> PhongMaterial(color()) - is MetaItem.NodeItem -> PhongMaterial().apply { + is ValueItem -> PhongMaterial(color()) + is NodeItem -> PhongMaterial().apply { val opacity = node[SolidMaterial.OPACITY_KEY].double ?: 1.0 diffuseColor = node[SolidMaterial.COLOR_KEY]?.color(opacity) ?: Color.DARKGREY specularColor = node[SolidMaterial.SPECULAR_COLOR_KEY]?.color(opacity) ?: Color.WHITE diff --git a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FXReferenceFactory.kt b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FXReferenceFactory.kt index 7c7258ac..14845845 100644 --- a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FXReferenceFactory.kt +++ b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/FXReferenceFactory.kt @@ -6,15 +6,15 @@ import javafx.scene.Group import javafx.scene.Node import kotlin.reflect.KClass -class FXReferenceFactory(val plugin: FX3DPlugin) : FX3DFactory { - override val type: KClass get() = SolidReference::class +class FXReferenceFactory(val plugin: FX3DPlugin) : FX3DFactory { + override val type: KClass get() = SolidReferenceGroup::class - override fun invoke(obj: SolidReference, binding: VisualObjectFXBinding): Node { + override fun invoke(obj: SolidReferenceGroup, binding: VisualObjectFXBinding): Node { val prototype = obj.prototype val node = plugin.buildNode(prototype) - obj.onPropertyChange(this) { name-> - if (name.firstOrNull()?.body == SolidReference.REFERENCE_CHILD_PROPERTY_PREFIX) { + obj.onPropertyChange(plugin.context) { name-> + if (name.firstOrNull()?.body == SolidReferenceGroup.REFERENCE_CHILD_PROPERTY_PREFIX) { val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for reference child property: '$name'") val propertyName = name.cutFirst() val referenceChild = obj[childName] ?: error("Reference child with name '$childName' not found") diff --git a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/VisualObjectFXBinding.kt b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/VisualObjectFXBinding.kt index e5c19dd1..f6f72e54 100644 --- a/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/VisualObjectFXBinding.kt +++ b/visionforge-fx/src/main/kotlin/hep/dataforge/vision/solid/VisualObjectFXBinding.kt @@ -12,11 +12,11 @@ import tornadofx.* /** * A caching binding collection for [Vision] properties */ -class VisualObjectFXBinding(val obj: Vision) { - private val bindings = HashMap?>>() +class VisualObjectFXBinding(val fx: FX3DPlugin, val obj: Vision) { + private val bindings = HashMap>() init { - obj.onPropertyChange(this) { name -> + obj.onPropertyChange(fx.context) { name -> bindings.filter { it.key.startsWith(name) }.forEach { entry -> Platform.runLater { entry.value.invalidate() @@ -31,10 +31,10 @@ class VisualObjectFXBinding(val obj: Vision) { } } - operator fun get(key: Name): ObjectBinding?> { + operator fun get(key: Name): ObjectBinding { return bindings.getOrPut(key) { - object : ObjectBinding?>() { - override fun computeValue(): MetaItem<*>? = obj.getProperty(key) + object : ObjectBinding() { + override fun computeValue(): MetaItem? = obj.getProperty(key) } } } @@ -42,19 +42,19 @@ class VisualObjectFXBinding(val obj: Vision) { operator fun get(key: String) = get(key.toName()) } -fun ObjectBinding?>.value() = objectBinding { it.value } -fun ObjectBinding?>.string() = stringBinding { it.string } -fun ObjectBinding?>.number() = objectBinding { it.number } -fun ObjectBinding?>.double() = objectBinding { it.double } -fun ObjectBinding?>.float() = objectBinding { it.float } -fun ObjectBinding?>.int() = objectBinding { it.int } -fun ObjectBinding?>.long() = objectBinding { it.long } -fun ObjectBinding?>.node() = objectBinding { it.node } +fun ObjectBinding.value() = objectBinding { it.value } +fun ObjectBinding.string() = stringBinding { it.string } +fun ObjectBinding.number() = objectBinding { it.number } +fun ObjectBinding.double() = objectBinding { it.double } +fun ObjectBinding.float() = objectBinding { it.float } +fun ObjectBinding.int() = objectBinding { it.int } +fun ObjectBinding.long() = objectBinding { it.long } +fun ObjectBinding.node() = objectBinding { it.node } -fun ObjectBinding?>.string(default: String) = stringBinding { it.string ?: default } -fun ObjectBinding?>.double(default: Double) = doubleBinding { it.double ?: default } -fun ObjectBinding?>.float(default: Float) = floatBinding { it.float ?: default } -fun ObjectBinding?>.int(default: Int) = integerBinding { it.int ?: default } -fun ObjectBinding?>.long(default: Long) = longBinding { it.long ?: default } +fun ObjectBinding.string(default: String) = stringBinding { it.string ?: default } +fun ObjectBinding.double(default: Double) = doubleBinding { it.double ?: default } +fun ObjectBinding.float(default: Float) = floatBinding { it.float ?: default } +fun ObjectBinding.int(default: Int) = integerBinding { it.int ?: default } +fun ObjectBinding.long(default: Long) = longBinding { it.long ?: default } -fun ObjectBinding?>.transform(transform: (MetaItem<*>) -> T) = objectBinding { it?.let(transform) } +fun ObjectBinding.transform(transform: (MetaItem) -> T) = objectBinding { it?.let(transform) } diff --git a/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GDMLTransformer.kt b/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GDMLTransformer.kt index d3dd5b3a..2ac28d6b 100644 --- a/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GDMLTransformer.kt +++ b/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GDMLTransformer.kt @@ -2,12 +2,12 @@ package hep.dataforge.vision.gdml import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaBuilder -import hep.dataforge.meta.set import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.names.plus import hep.dataforge.names.toName import hep.dataforge.vision.set +import hep.dataforge.vision.setProperty import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.SolidMaterial.Companion.MATERIAL_COLOR_KEY import hep.dataforge.vision.styleSheet @@ -38,7 +38,6 @@ public class GDMLTransformerSettings { public var solidAction: (GDMLSolid) -> Action = { Action.PROTOTYPE } public var volumeAction: (GDMLGroup) -> Action = { Action.PROTOTYPE } - } private class GDMLTransformer(val settings: GDMLTransformerSettings) { @@ -51,13 +50,13 @@ private class GDMLTransformer(val settings: GDMLTransformerSettings) { private val proto = SolidGroup() private val solids = proto.group(solidsName) { - config["edges.enabled"] = false + setProperty("edges.enabled", false) } - private val referenceStore = HashMap>() + private val referenceStore = HashMap>() - private fun proxySolid(root: GDML, group: SolidGroup, solid: GDMLSolid, name: String): SolidReference { + private fun proxySolid(root: GDML, group: SolidGroup, solid: GDMLSolid, name: String): SolidReferenceGroup { val templateName = solidsName + name if (proto[templateName] == null) { solids.addSolid(root, solid, name) @@ -67,7 +66,7 @@ private class GDMLTransformer(val settings: GDMLTransformerSettings) { return ref } - private fun proxyVolume(root: GDML, group: SolidGroup, physVolume: GDMLPhysVolume, volume: GDMLGroup): SolidReference { + private fun proxyVolume(root: GDML, group: SolidGroup, physVolume: GDMLPhysVolume, volume: GDMLGroup): SolidReferenceGroup { val templateName = volumesName + volume.name.asName() if (proto[templateName] == null) { proto[templateName] = volume(root, volume) @@ -324,7 +323,7 @@ private class GDMLTransformer(val settings: GDMLTransformerSettings) { } } - fun finalize(final: SolidGroup): SolidGroup { + private fun finalize(final: SolidGroup): SolidGroup { //final.prototypes = proto final.useStyle("GDML") { Solid.ROTATION_ORDER_KEY put RotationOrder.ZXY diff --git a/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GdmlOptimizer.kt b/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GdmlOptimizer.kt index 692c334b..a01e3db0 100644 --- a/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GdmlOptimizer.kt +++ b/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GdmlOptimizer.kt @@ -1,17 +1,9 @@ package hep.dataforge.vision.gdml import hep.dataforge.meta.DFExperimental -import hep.dataforge.meta.sequence -import hep.dataforge.meta.set -import hep.dataforge.names.Name -import hep.dataforge.names.toName -import hep.dataforge.vision.* +import hep.dataforge.meta.itemSequence +import hep.dataforge.vision.Vision import hep.dataforge.vision.solid.* -import hep.dataforge.vision.visitor.VisionVisitor -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import mu.KotlinLogging public expect class Counter() { public fun get(): Int @@ -24,6 +16,7 @@ private fun Point3D?.safePlus(other: Point3D?): Point3D? = if (this == null && o (this ?: Point3D(0, 0, 0)) + (other ?: Point3D(0, 0, 0)) } +@DFExperimental internal fun Vision.updateFrom(other: Vision): Vision { if (this is Solid && other is Solid) { position = position.safePlus(other.position) @@ -33,9 +26,9 @@ internal fun Vision.updateFrom(other: Vision): Vision { scaleY = scaleY.toDouble() * other.scaleY.toDouble() scaleZ = scaleZ.toDouble() * other.scaleZ.toDouble() } - other.properties?.sequence()?.forEach { (name, item) -> - if (properties?.getItem(name) == null) { - config[name] = item + other.meta.itemSequence().forEach { (name, item) -> + if (getProperty(name) == null) { + setProperty(name, item) } } } diff --git a/visionforge-plotly/build.gradle.kts b/visionforge-plotly/build.gradle.kts new file mode 100644 index 00000000..51419b1e --- /dev/null +++ b/visionforge-plotly/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("ru.mipt.npm.mpp") +} + +kscience { + useSerialization() +} + +val plotlyVersion = "0.3.1-dev" + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":visionforge-core")) + api("kscience.plotlykt:plotlykt-core:${plotlyVersion}") + } + } + } +} \ No newline at end of file diff --git a/visionforge-plotly/src/commonMain/kotlin/hep/dataforge/vision/plotly/VisionOfPlotly.kt b/visionforge-plotly/src/commonMain/kotlin/hep/dataforge/vision/plotly/VisionOfPlotly.kt new file mode 100644 index 00000000..997a560a --- /dev/null +++ b/visionforge-plotly/src/commonMain/kotlin/hep/dataforge/vision/plotly/VisionOfPlotly.kt @@ -0,0 +1,12 @@ +package hep.dataforge.vision.plotly + +import hep.dataforge.meta.DFExperimental +import hep.dataforge.vision.VisionBase +import hep.dataforge.vision.html.VisionOutput +import kscience.plotly.Plot +import kscience.plotly.Plotly + +public class VisionOfPlotly(public val plot: Plot): VisionBase(plot.config) + +@DFExperimental +public inline fun VisionOutput.plotly(block: Plot.() -> Unit): VisionOfPlotly = VisionOfPlotly(Plotly.plot(block)) \ No newline at end of file diff --git a/visionforge-plotly/src/jsMain/kotlin/hep/dataforge/vision/plotly/PlotlyPlugin.kt b/visionforge-plotly/src/jsMain/kotlin/hep/dataforge/vision/plotly/PlotlyPlugin.kt new file mode 100644 index 00000000..9e2793dc --- /dev/null +++ b/visionforge-plotly/src/jsMain/kotlin/hep/dataforge/vision/plotly/PlotlyPlugin.kt @@ -0,0 +1,33 @@ +package hep.dataforge.vision.plotly + +import hep.dataforge.context.AbstractPlugin +import hep.dataforge.context.Context +import hep.dataforge.context.PluginFactory +import hep.dataforge.context.PluginTag +import hep.dataforge.meta.Meta +import hep.dataforge.vision.Vision +import hep.dataforge.vision.client.ElementVisionRenderer +import kscience.plotly.PlotlyConfig +import kscience.plotly.plot +import org.w3c.dom.Element +import kotlin.reflect.KClass + +public class PlotlyPlugin : AbstractPlugin(), ElementVisionRenderer { + + override val tag: PluginTag get() = Companion.tag + + override fun rateVision(vision: Vision): Int = + if (vision is VisionOfPlotly) ElementVisionRenderer.DEFAULT_RATING else ElementVisionRenderer.ZERO_RATING + + override fun render(element: Element, vision: Vision, meta: Meta) { + val plot = (vision as? VisionOfPlotly)?.plot ?: error("Only VisionOfPlotly visions are supported") + val config = PlotlyConfig.read(meta) + element.plot(plot, config) + } + + public companion object : PluginFactory { + override val tag: PluginTag = PluginTag("vision.plotly", PluginTag.DATAFORGE_GROUP) + override val type: KClass = PlotlyPlugin::class + override fun invoke(meta: Meta, context: Context): PlotlyPlugin = PlotlyPlugin() + } +} \ No newline at end of file diff --git a/visionforge-plotly/src/jsMain/kotlin/hep/dataforge/vision/plotly/jsMain.kt b/visionforge-plotly/src/jsMain/kotlin/hep/dataforge/vision/plotly/jsMain.kt new file mode 100644 index 00000000..ae0ffb37 --- /dev/null +++ b/visionforge-plotly/src/jsMain/kotlin/hep/dataforge/vision/plotly/jsMain.kt @@ -0,0 +1,15 @@ +package hep.dataforge.vision.plotly + +//public fun main() { +// val visionContext: Context = Global.context("vision-client") +// +// //Loading three-js renderer +// val threePlugin = visionContext.plugins.fetch(PlotlyPlugin) +// +// val clientManager = visionContext.plugins.fetch(VisionClient) +// +// //Fetch from server and render visions for all outputs +// window.onload = { +// clientManager.renderAllVisions() +// } +//} \ No newline at end of file diff --git a/visionforge-server/src/main/kotlin/hep/dataforge/vision/three/server/VisionServer.kt b/visionforge-server/src/main/kotlin/hep/dataforge/vision/three/server/VisionServer.kt index eaa38618..5baf2643 100644 --- a/visionforge-server/src/main/kotlin/hep/dataforge/vision/three/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/hep/dataforge/vision/three/server/VisionServer.kt @@ -41,6 +41,13 @@ import java.awt.Desktop import java.net.URI import kotlin.time.milliseconds +public enum class VisionServerDataMode { + EMBED, + FETCH, + CONNECT +} + + /** * A ktor plugin container with given [routing] */ @@ -52,6 +59,7 @@ public class VisionServer internal constructor( override val config: Config = Config() public var updateInterval: Long by config.long(300, key = UPDATE_INTERVAL_KEY) public var cacheFragments: Boolean by config.boolean(true) + public var dataMode: VisionServerDataMode = VisionServerDataMode.CONNECT /** * a list of headers that should be applied to all pages @@ -72,10 +80,23 @@ public class VisionServer internal constructor( val consumer = object : VisionTagConsumer(consumer) { override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) { visionMap[name] = vision - - // Toggle updates - attributes[OUTPUT_FETCH_ATTRIBUTE] = "true" - attributes[OUTPUT_CONNECT_ATTRIBUTE] = "true" + // Toggle update mode + when (dataMode) { + VisionServerDataMode.EMBED -> { + script { + attributes["class"] = OUTPUT_DATA_CLASS + unsafe { + +visionManager.encodeToString(vision) + } + } + } + VisionServerDataMode.FETCH -> { + attributes[OUTPUT_FETCH_ATTRIBUTE] = "auto" + } + VisionServerDataMode.CONNECT -> { + attributes[OUTPUT_CONNECT_ATTRIBUTE] = "auto" + } + } } } @@ -110,10 +131,11 @@ public class VisionServer internal constructor( application.log.debug("Opened server socket for $name") val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered") + try { withContext(visionManager.context.coroutineContext) { vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update -> - val json = VisionManager.defaultJson.encodeToString( + val json = visionManager.jsonFormat.encodeToString( VisionChange.serializer(), update ) diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/ColorAccessor.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/ColorAccessor.kt new file mode 100644 index 00000000..05e8cad9 --- /dev/null +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/ColorAccessor.kt @@ -0,0 +1,51 @@ +package hep.dataforge.vision.solid + +import hep.dataforge.meta.MutableItemProvider +import hep.dataforge.meta.set +import hep.dataforge.meta.value +import hep.dataforge.names.Name +import hep.dataforge.values.Value +import hep.dataforge.values.asValue +import hep.dataforge.values.string +import hep.dataforge.vision.Colors +import hep.dataforge.vision.VisionBuilder + +@VisionBuilder +public class ColorAccessor(private val parent: MutableItemProvider, private val colorKey: Name) { + public var value: Value? + get() = parent.getItem(colorKey).value + set(value) { + parent[colorKey] = value + } +} + +public var ColorAccessor?.string: String? + get() = this?.value?.string + set(value) { + this?.value = value?.asValue() + } + +/** + * Set [webcolor](https://en.wikipedia.org/wiki/Web_colors) as string + */ +public operator fun ColorAccessor?.invoke(webColor: String) { + this?.value = webColor.asValue() +} + +/** + * Set color as RGB integer + */ +public operator fun ColorAccessor?.invoke(rgb: Int) { + this?.value = Colors.rgbToString(rgb).asValue() +} + +/** + * Set color as RGB + */ +public operator fun ColorAccessor?.invoke(r: UByte, g: UByte, b: UByte) { + this?.value = Colors.rgbToString(r, g, b).asValue() +} + +public fun ColorAccessor?.clear() { + this?.value = null +} \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Composite.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Composite.kt index 9264ee99..6dc96076 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Composite.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Composite.kt @@ -1,4 +1,3 @@ - package hep.dataforge.vision.solid import hep.dataforge.meta.update @@ -18,7 +17,7 @@ public enum class CompositeType { public class Composite( public val compositeType: CompositeType, public val first: Solid, - public val second: Solid + public val second: Solid, ) : SolidBase(), Solid, VisionGroup { init { @@ -34,25 +33,25 @@ public class Composite( public inline fun VisionContainerBuilder.composite( type: CompositeType, name: String = "", - builder: SolidGroup.() -> Unit + builder: SolidGroup.() -> Unit, ): Composite { val group = SolidGroup().apply(builder) val children = group.children.values.filterIsInstance() if (children.size != 2) error("Composite requires exactly two children") - return Composite(type, children[0], children[1]).also { - it.config.update(group.config) - //it.material = group.material - + return Composite(type, children[0], children[1]).also { composite -> + composite.configure { + update(group.meta) + } if (group.position != null) { - it.position = group.position + composite.position = group.position } if (group.rotation != null) { - it.rotation = group.rotation + composite.rotation = group.rotation } if (group.scale != null) { - it.scale = group.scale + composite.scale = group.scale } - set(name, it) + set(name, composite) } } @@ -65,5 +64,8 @@ public inline fun VisionContainerBuilder.subtract(name: String = "", buil composite(CompositeType.SUBTRACT, name, builder = builder) @VisionBuilder -public inline fun VisionContainerBuilder.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite = +public inline fun VisionContainerBuilder.intersect( + name: String = "", + builder: SolidGroup.() -> Unit, +): Composite = composite(CompositeType.INTERSECT, name, builder = builder) \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/PolyLine.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/PolyLine.kt index 998f7f4f..28f4ad68 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/PolyLine.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/PolyLine.kt @@ -6,7 +6,7 @@ import hep.dataforge.names.asName import hep.dataforge.names.plus import hep.dataforge.vision.VisionBuilder import hep.dataforge.vision.VisionContainerBuilder -import hep.dataforge.vision.props +import hep.dataforge.vision.allProperties import hep.dataforge.vision.set import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -16,7 +16,7 @@ import kotlinx.serialization.Serializable public class PolyLine(public var points: List) : SolidBase(), Solid { //var lineType by string() - public var thickness: Number by props().number(1.0, key = SolidMaterial.MATERIAL_KEY + THICKNESS_KEY) + public var thickness: Number by allProperties(inherit = false).number(1.0, key = SolidMaterial.MATERIAL_KEY + THICKNESS_KEY) public companion object { public val THICKNESS_KEY: Name = "thickness".asName() diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt index 278cc9e7..1050dff1 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt @@ -1,18 +1,17 @@ package hep.dataforge.vision.solid -import hep.dataforge.meta.* +import hep.dataforge.meta.boolean import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.enum +import hep.dataforge.meta.int import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.names.plus import hep.dataforge.values.ValueType import hep.dataforge.values.asValue -import hep.dataforge.vision.Vision +import hep.dataforge.vision.* import hep.dataforge.vision.Vision.Companion.VISIBLE_KEY -import hep.dataforge.vision.VisionBuilder -import hep.dataforge.vision.enum import hep.dataforge.vision.layout.Output -import hep.dataforge.vision.setProperty import hep.dataforge.vision.solid.Solid.Companion.DETAIL_KEY import hep.dataforge.vision.solid.Solid.Companion.IGNORE_KEY import hep.dataforge.vision.solid.Solid.Companion.LAYER_KEY @@ -45,13 +44,13 @@ public interface Solid : Vision { public val Y_POSITION_KEY: Name = POSITION_KEY + Y_KEY public val Z_POSITION_KEY: Name = POSITION_KEY + Z_KEY - public val ROTATION: Name = "rotation".asName() + public val ROTATION_KEY: Name = "rotation".asName() - public val X_ROTATION_KEY: Name = ROTATION + X_KEY - public val Y_ROTATION_KEY: Name = ROTATION + Y_KEY - public val Z_ROTATION_KEY: Name = ROTATION + Z_KEY + public val X_ROTATION_KEY: Name = ROTATION_KEY + X_KEY + public val Y_ROTATION_KEY: Name = ROTATION_KEY + Y_KEY + public val Z_ROTATION_KEY: Name = ROTATION_KEY + Z_KEY - public val ROTATION_ORDER_KEY: Name = ROTATION + "order" + public val ROTATION_ORDER_KEY: Name = ROTATION_KEY + "order" public val SCALE_KEY: Name = "scale".asName() @@ -62,6 +61,7 @@ public interface Solid : Vision { public val descriptor: NodeDescriptor by lazy { NodeDescriptor { value(VISIBLE_KEY) { + inherited = false type(ValueType.BOOLEAN) default(true) } @@ -70,11 +70,14 @@ public interface Solid : Vision { value(Vision.STYLE_KEY) { type(ValueType.STRING) multiple = true + hide() } item(SolidMaterial.MATERIAL_KEY.toString(), SolidMaterial.descriptor) - enum(ROTATION_ORDER_KEY, default = RotationOrder.XYZ) + enum(ROTATION_ORDER_KEY, default = RotationOrder.XYZ) { + hide() + } } } @@ -82,7 +85,7 @@ public interface Solid : Vision { if (first.position != second.position) return false if (first.rotation != second.rotation) return false if (first.scale != second.scale) return false - if (first.properties != second.properties) return false + if (first.meta != second.meta) return false return true } @@ -90,7 +93,7 @@ public interface Solid : Vision { var result = +(solid.position?.hashCode() ?: 0) result = 31 * result + (solid.rotation?.hashCode() ?: 0) result = 31 * result + (solid.scale?.hashCode() ?: 0) - result = 31 * result + (solid.properties?.hashCode() ?: 0) + result = 31 * result + solid.allProperties().hashCode() return result } } @@ -100,9 +103,9 @@ public interface Solid : Vision { * Get the layer number this solid belongs to. Return 0 if layer is not defined. */ public var Solid.layer: Int - get() = properties?.getItem(LAYER_KEY).int ?: 0 + get() = allProperties().getItem(LAYER_KEY).int ?: 0 set(value) { - config[LAYER_KEY] = value.asValue() + setProperty(LAYER_KEY, value) } @VisionBuilder @@ -153,21 +156,21 @@ public var Solid.x: Number get() = position?.x ?: 0f set(value) { position().x = value.toDouble() - propertyChanged(Solid.X_POSITION_KEY) + asyncNotifyPropertyChange(Solid.X_POSITION_KEY) } public var Solid.y: Number get() = position?.y ?: 0f set(value) { position().y = value.toDouble() - propertyChanged(Solid.Y_POSITION_KEY) + asyncNotifyPropertyChange(Solid.Y_POSITION_KEY) } public var Solid.z: Number get() = position?.z ?: 0f set(value) { position().z = value.toDouble() - propertyChanged(Solid.Z_POSITION_KEY) + asyncNotifyPropertyChange(Solid.Z_POSITION_KEY) } private fun Solid.rotation(): Point3D = @@ -177,21 +180,21 @@ public var Solid.rotationX: Number get() = rotation?.x ?: 0f set(value) { rotation().x = value.toDouble() - propertyChanged(Solid.X_ROTATION_KEY) + asyncNotifyPropertyChange(Solid.X_ROTATION_KEY) } public var Solid.rotationY: Number get() = rotation?.y ?: 0f set(value) { rotation().y = value.toDouble() - propertyChanged(Solid.Y_ROTATION_KEY) + asyncNotifyPropertyChange(Solid.Y_ROTATION_KEY) } public var Solid.rotationZ: Number get() = rotation?.z ?: 0f set(value) { rotation().z = value.toDouble() - propertyChanged(Solid.Z_ROTATION_KEY) + asyncNotifyPropertyChange(Solid.Z_ROTATION_KEY) } private fun Solid.scale(): Point3D = @@ -201,19 +204,19 @@ public var Solid.scaleX: Number get() = scale?.x ?: 1f set(value) { scale().x = value.toDouble() - propertyChanged(Solid.X_SCALE_KEY) + asyncNotifyPropertyChange(Solid.X_SCALE_KEY) } public var Solid.scaleY: Number get() = scale?.y ?: 1f set(value) { scale().y = value.toDouble() - propertyChanged(Solid.Y_SCALE_KEY) + asyncNotifyPropertyChange(Solid.Y_SCALE_KEY) } public var Solid.scaleZ: Number get() = scale?.z ?: 1f set(value) { scale().z = value.toDouble() - propertyChanged(Solid.Z_SCALE_KEY) + asyncNotifyPropertyChange(Solid.Z_SCALE_KEY) } \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidBase.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidBase.kt index b9471724..21c8e5bf 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidBase.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidBase.kt @@ -35,6 +35,6 @@ internal fun Meta.toVector(default: Float = 0f) = Point3D( internal fun Solid.updatePosition(meta: Meta?) { meta[Solid.POSITION_KEY].node?.toVector()?.let { position = it } - meta[Solid.ROTATION].node?.toVector()?.let { rotation = it } + meta[Solid.ROTATION_KEY].node?.toVector()?.let { rotation = it } meta[Solid.SCALE_KEY].node?.toVector(1f)?.let { scale = it } } \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidGroup.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidGroup.kt index b87cdaab..578a06e6 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidGroup.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidGroup.kt @@ -1,6 +1,6 @@ package hep.dataforge.vision.solid -import hep.dataforge.meta.Config +import hep.dataforge.meta.MetaItem import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.names.Name import hep.dataforge.names.NameToken @@ -15,30 +15,35 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder public interface PrototypeHolder { - public val parent: VisionGroup? - public val prototypes: MutableVisionGroup? + @VisionBuilder + public fun prototypes(builder: VisionContainerBuilder.() -> Unit) + + public fun getPrototype(name: Name): Solid? } /** * Represents 3-dimensional Visual Group + * @param prototypes A container for templates visible inside this group */ @Serializable @SerialName("group.solid") -public class SolidGroup : VisionGroupBase(), Solid, PrototypeHolder { +public class SolidGroup( + @Serializable(Prototypes.Companion::class) @SerialName("prototypes") private var prototypes: MutableVisionGroup? = null, +) : VisionGroupBase(), Solid, PrototypeHolder { override val descriptor: NodeDescriptor get() = Solid.descriptor + /** - * A container for templates visible inside this group + * Ger a prototype redirecting the request to the parent if prototype is not found */ - @Serializable(Prototypes.Companion::class) - override var prototypes: MutableVisionGroup? = null - private set + override fun getPrototype(name: Name): Solid? = + (prototypes?.get(name) as? Solid) ?: (parent as? PrototypeHolder)?.getPrototype(name) /** * Create or edit prototype node as a group */ - public fun prototypes(builder: VisionContainerBuilder.() -> Unit): Unit { + override fun prototypes(builder: VisionContainerBuilder.() -> Unit): Unit { (prototypes ?: Prototypes().also { prototypes = it it.parent = this @@ -51,13 +56,6 @@ public class SolidGroup : VisionGroupBase(), Solid, PrototypeHolder { override var scale: Point3D? = null - override fun attachChildren() { - prototypes?.parent = this - prototypes?.attachChildren() - super.attachChildren() - } - - // /** // * TODO add special static group to hold statics without propagation // */ @@ -80,19 +78,17 @@ public fun SolidGroup(block: SolidGroup.() -> Unit): SolidGroup { return SolidGroup().apply(block) } -/** - * Ger a prototype redirecting the request to the parent if prototype is not found - */ -public tailrec fun PrototypeHolder.getPrototype(name: Name): Solid? = - prototypes?.get(name) as? Solid ?: (parent as? PrototypeHolder)?.getPrototype(name) - @VisionBuilder -public fun VisionContainerBuilder.group(name: Name = Name.EMPTY, action: SolidGroup.() -> Unit = {}): SolidGroup = +public fun VisionContainerBuilder.group( + name: Name = Name.EMPTY, + action: SolidGroup.() -> Unit = {}, +): SolidGroup = SolidGroup().apply(action).also { set(name, it) } /** * Define a group with given [name], attach it to this parent and return it. */ +@VisionBuilder public fun VisionContainerBuilder.group(name: String, action: SolidGroup.() -> Unit = {}): SolidGroup = SolidGroup().apply(action).also { set(name, it) } @@ -102,27 +98,30 @@ public fun VisionContainerBuilder.group(name: String, action: SolidGroup @Serializable(Prototypes.Companion::class) internal class Prototypes( children: Map = emptyMap(), -) : VisionGroupBase(), PrototypeHolder { +) : VisionGroupBase(children as? MutableMap ?: children.toMutableMap()), PrototypeHolder { init { - this.childrenInternal.putAll(children) - } - - override var properties: Config? - get() = null - set(_) { - error("Can't define properties for prototypes block") - } - - override val prototypes: MutableVisionGroup get() = this - - override fun attachChildren() { + //used during deserialization only children.values.forEach { it.parent = parent - (it as? VisionGroup)?.attachChildren() } } + override fun getOwnProperty(name: Name): MetaItem? = null + + override fun getProperty( + name: Name, + inherit: Boolean, + includeStyles: Boolean, + includeDefaults: Boolean, + ): MetaItem? = null + + override fun setProperty(name: Name, item: MetaItem?, notify: Boolean) { + error("Can't ser property of prototypes container") + } + + override val descriptor: NodeDescriptor? = null + companion object : KSerializer { private val mapSerializer: KSerializer> = @@ -142,4 +141,10 @@ internal class Prototypes( mapSerializer.serialize(encoder, value.children) } } + + override fun prototypes(builder: VisionContainerBuilder.() -> Unit) { + apply(builder) + } + + override fun getPrototype(name: Name): Solid? = get(name) as? Solid } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidManager.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidManager.kt index a01cc961..f55dc1f4 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidManager.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidManager.kt @@ -4,11 +4,16 @@ import hep.dataforge.context.AbstractPlugin import hep.dataforge.context.Context import hep.dataforge.context.PluginFactory import hep.dataforge.context.PluginTag +import hep.dataforge.meta.DFExperimental import hep.dataforge.meta.Meta import hep.dataforge.names.Name import hep.dataforge.names.toName -import hep.dataforge.vision.* +import hep.dataforge.vision.Vision +import hep.dataforge.vision.VisionBase +import hep.dataforge.vision.VisionGroupBase +import hep.dataforge.vision.VisionManager import hep.dataforge.vision.VisionManager.Companion.VISION_SERIALIZER_MODULE_TARGET +import hep.dataforge.vision.html.VisionOutput import kotlinx.serialization.PolymorphicSerializer import kotlinx.serialization.json.Json import kotlinx.serialization.modules.PolymorphicModuleBuilder @@ -30,13 +35,13 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) { } public companion object : PluginFactory { - override val tag: PluginTag = PluginTag(name = "visual.spatial", group = PluginTag.DATAFORGE_GROUP) + override val tag: PluginTag = PluginTag(name = "vision.solid", group = PluginTag.DATAFORGE_GROUP) override val type: KClass = SolidManager::class override fun invoke(meta: Meta, context: Context): SolidManager = SolidManager(meta) private fun PolymorphicModuleBuilder.solids() { subclass(SolidGroup.serializer()) - subclass(SolidReference.serializer()) + subclass(SolidReferenceGroup.serializer()) subclass(Composite.serializer()) subclass(Tube.serializer()) subclass(Box.serializer()) @@ -67,10 +72,9 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) { public fun encodeToString(solid: Solid): String = jsonForSolids.encodeToString(PolymorphicSerializer(Vision::class), solid) - fun decodeFromString(str: String): Solid = jsonForSolids.decodeFromString(PolymorphicSerializer(Solid::class), str).also { - if(it is VisionGroup){ - it.attachChildren() - } - } + public fun decodeFromString(str: String): Solid = jsonForSolids.decodeFromString(PolymorphicSerializer(Solid::class), str) } } + +@DFExperimental +public inline fun VisionOutput.solid(block: SolidGroup.() -> Unit): SolidGroup = SolidGroup().apply(block) \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidMaterial.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidMaterial.kt index aebd168d..3062b0ad 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidMaterial.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidMaterial.kt @@ -6,53 +6,12 @@ import hep.dataforge.meta.descriptors.attributes import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.names.plus -import hep.dataforge.values.Value import hep.dataforge.values.ValueType import hep.dataforge.values.asValue -import hep.dataforge.values.string -import hep.dataforge.vision.Colors -import hep.dataforge.vision.VisionBuilder -import hep.dataforge.vision.setProperty +import hep.dataforge.vision.* import hep.dataforge.vision.solid.SolidMaterial.Companion.MATERIAL_COLOR_KEY import hep.dataforge.vision.solid.SolidMaterial.Companion.MATERIAL_KEY import hep.dataforge.vision.solid.SolidMaterial.Companion.MATERIAL_OPACITY_KEY -import hep.dataforge.vision.widgetType - -@VisionBuilder -public class ColorAccessor(private val parent: MutableItemProvider, private val colorKey: Name) { - public var value: Value? - get() = parent.getItem(colorKey).value - set(value) { - parent[colorKey] = value - } -} - -public var ColorAccessor?.string: String? - get() = this?.value?.string - set(value) { - this?.value = value?.asValue() - } - -/** - * Set [webcolor](https://en.wikipedia.org/wiki/Web_colors) as string - */ -public operator fun ColorAccessor?.invoke(webColor: String) { - this?.value = webColor.asValue() -} - -/** - * Set color as RGB integer - */ -public operator fun ColorAccessor?.invoke(rgb: Int) { - this?.value = rgb.asValue() -} - -/** - * Set color as RGB - */ -public operator fun ColorAccessor?.invoke(r: UByte, g: UByte, b: UByte) { - this?.value = Colors.rgbToString(r, g, b).asValue() -} @VisionBuilder public class SolidMaterial : Scheme() { @@ -60,12 +19,12 @@ public class SolidMaterial : Scheme() { /** * Primary web-color for the material */ - public var color: ColorAccessor = ColorAccessor(config, COLOR_KEY) + public val color: ColorAccessor = ColorAccessor(this, COLOR_KEY) /** * Specular color for phong material */ - public var specularColor: ColorAccessor = ColorAccessor(config, SPECULAR_COLOR_KEY) + public val specularColor: ColorAccessor = ColorAccessor(this, SPECULAR_COLOR_KEY) /** * Opacity @@ -92,11 +51,27 @@ public class SolidMaterial : Scheme() { public override val descriptor: NodeDescriptor by lazy { //must be lazy to avoid initialization bug NodeDescriptor { + inherited = true + usesStyles = true + value(COLOR_KEY) { + inherited = true + usesStyles = true type(ValueType.STRING, ValueType.NUMBER) widgetType = "color" } + + value(SPECULAR_COLOR_KEY) { + inherited = true + usesStyles = true + type(ValueType.STRING, ValueType.NUMBER) + widgetType = "color" + hide() + } + value(OPACITY_KEY) { + inherited = true + usesStyles = true type(ValueType.NUMBER) default(1.0) attributes { @@ -107,6 +82,8 @@ public class SolidMaterial : Scheme() { widgetType = "slider" } value(WIREFRAME_KEY) { + inherited = true + usesStyles = true type(ValueType.BOOLEAN) default(false) } @@ -115,24 +92,23 @@ public class SolidMaterial : Scheme() { } } -public val Solid.color: ColorAccessor get() = ColorAccessor(config, MATERIAL_COLOR_KEY) +public val Solid.color: ColorAccessor + get() = ColorAccessor( + allProperties(inherit = true), + MATERIAL_COLOR_KEY + ) public var Solid.material: SolidMaterial? - get() = getProperty(MATERIAL_KEY).node?.let { SolidMaterial.read(it) } - set(value) = setProperty(MATERIAL_KEY, value?.config) + get() = getProperty(MATERIAL_KEY, inherit = true).node?.let { SolidMaterial.read(it) } + set(value) = setProperty(MATERIAL_KEY, value?.rootNode) @VisionBuilder public fun Solid.material(builder: SolidMaterial.() -> Unit) { - val node = config[MATERIAL_KEY].node - if (node != null) { - SolidMaterial.update(node, builder) - } else { - config[MATERIAL_KEY] = SolidMaterial(builder) - } + ownProperties.getChild(MATERIAL_KEY).update(SolidMaterial, builder) } public var Solid.opacity: Number? - get() = getProperty(MATERIAL_OPACITY_KEY).number + get() = getProperty(MATERIAL_OPACITY_KEY, inherit = true).number set(value) { setProperty(MATERIAL_OPACITY_KEY, value?.asValue()) } \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt index 4df85142..e49d8d0f 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt @@ -3,43 +3,32 @@ package hep.dataforge.vision.solid import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.names.* +import hep.dataforge.values.Null import hep.dataforge.vision.* +import kotlinx.coroutines.CoroutineScope import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import kotlin.collections.set -public abstract class AbstractReference : SolidBase(), VisionGroup { - public abstract val prototype: Solid +public interface SolidReference : Vision { + public val prototype: Solid +} - override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = sequence { - yield(properties?.get(name)) - yieldAll(getStyleItems(name)) - yield(prototype.getProperty(name)) +private fun SolidReference.getRefProperty( + name: Name, + inherit: Boolean, + includeStyles: Boolean, + includeDefaults: Boolean, +): MetaItem? { + return sequence { + yield(getOwnProperty(name)) + if (includeStyles) { + yieldAll(getStyleItems(name)) + } + yield(prototype.getProperty(name, inherit, includeStyles, includeDefaults)) if (inherit) { yield(parent?.getProperty(name, inherit)) } }.merge() - - override var styles: List - get() = (properties[Vision.STYLE_KEY]?.stringList ?: emptyList()) + prototype.styles - set(value) { - config[Vision.STYLE_KEY] = value - } - - override val allProperties: Laminate - get() = Laminate( - properties, - allStyles, - prototype.allProperties, - parent?.allProperties, - ) - - override fun attachChildren() { - //do nothing - } - - override val descriptor: NodeDescriptor get() = prototype.descriptor } /** @@ -47,9 +36,9 @@ public abstract class AbstractReference : SolidBase(), VisionGroup { */ @Serializable @SerialName("solid.ref") -public class SolidReference( +public class SolidReferenceGroup( public val templateName: Name, -) : AbstractReference(), Solid { +) : SolidBase(), SolidReference, VisionGroup { /** * Recursively search for defined template in the parent @@ -58,59 +47,102 @@ public class SolidReference( get() = (parent as? SolidGroup)?.getPrototype(templateName) ?: error("Prototype with name $templateName not found in $parent") - @Transient - private val propertyCache: HashMap = HashMap() - - - override val children: Map + override val children: Map get() = (prototype as? VisionGroup)?.children ?.filter { !it.key.toString().startsWith("@") } ?.mapValues { ReferenceChild(it.key.asName()) } ?: emptyMap() - private fun childPropertyName(childName: Name, propertyName: Name): Name { - return NameToken(REFERENCE_CHILD_PROPERTY_PREFIX, childName.toString()) + propertyName + private fun childToken(childName: Name): NameToken = + NameToken(REFERENCE_CHILD_PROPERTY_PREFIX, childName.toString()) + + private fun childPropertyName(childName: Name, propertyName: Name): Name = + childToken(childName) + propertyName + + private fun getChildProperty(childName: Name, propertyName: Name): MetaItem? { + return getOwnProperty(childPropertyName(childName, propertyName)) + } + + private fun setChildProperty(childName: Name, propertyName: Name, item: MetaItem?, notify: Boolean) { + setProperty(childPropertyName(childName, propertyName), item, notify) } private fun prototypeFor(name: Name): Solid { - return (prototype as? SolidGroup)?.get(name) as? Solid - ?: error("Prototype with name $name not found in $this") + return if (name.isEmpty()) prototype else { + (prototype as? SolidGroup)?.get(name) as? Solid + ?: error("Prototype with name $name not found in $this") + } } - //override fun findAllStyles(): Laminate = Laminate((styles + prototype.styles).mapNotNull { findStyle(it) }) + override fun getProperty( + name: Name, + inherit: Boolean, + includeStyles: Boolean, + includeDefaults: Boolean, + ): MetaItem? = getRefProperty(name, inherit, includeStyles, includeDefaults) + + override val descriptor: NodeDescriptor get() = prototype.descriptor + /** * A ProxyChild is created temporarily only to interact with properties, it does not store any values * (properties are stored in external cache) and created and destroyed on-demand). */ - public inner class ReferenceChild(public val name: Name) : AbstractReference() { + private inner class ReferenceChild(private val childName: Name) : SolidReference, VisionGroup { - override val prototype: Solid get() = prototypeFor(name) + override val prototype: Solid get() = prototypeFor(childName) override val children: Map get() = (prototype as? VisionGroup)?.children ?.filter { !it.key.toString().startsWith("@") } ?.mapValues { (key, _) -> - ReferenceChild(name + key.asName()) + ReferenceChild(childName + key.asName()) } ?: emptyMap() - override var properties: Config? - get() = propertyCache[name] + override val meta: Meta get() = TODO()// getChildProperty(childName, Name.EMPTY).node ?: Meta.EMPTY + + override fun getOwnProperty(name: Name): MetaItem? = getChildProperty(childName, name) + + override fun setProperty(name: Name, item: MetaItem?, notify: Boolean) { + setChildProperty(childName, name, item, notify) + } + + override fun getProperty( + name: Name, + inherit: Boolean, + includeStyles: Boolean, + includeDefaults: Boolean, + ): MetaItem? = getRefProperty(name, inherit, includeStyles, includeDefaults) + + override var parent: VisionGroup? + get() { + val parentName = childName.cutLast() + return if (parentName.isEmpty()) this@SolidReferenceGroup else ReferenceChild(parentName) + } set(value) { - if (value == null) { - propertyCache.remove(name)?.also { - //Removing listener if it is present - removeChangeListener(this@SolidReference) - } - } else { - propertyCache[name] = value.also { - onPropertyChange(this@SolidReference) { propertyName -> - this@SolidReference.propertyChanged(childPropertyName(name, propertyName)) - } - } + error("Setting a parent for a reference child is not possible") + } + + override fun onPropertyChange(scope: CoroutineScope, callback: suspend (Name) -> Unit) { + this@SolidReferenceGroup.onPropertyChange(scope) { name -> + if (name.startsWith(childToken(childName))) { + callback(name.cutFirst()) } } + } + + override suspend fun notifyPropertyChanged(propertyName: Name) { + this@SolidReferenceGroup.notifyPropertyChanged(childPropertyName(childName, propertyName)) + } + + override fun update(change: VisionChange) { + change.properties?.let { + updateProperties(Name.EMPTY, it.asMetaItem()) + } + } + + override val descriptor: NodeDescriptor get() = prototype.descriptor } @@ -120,13 +152,10 @@ public class SolidReference( } /** - * Get a vision prototype if it is a [SolidReference] or vision itself if it is not + * Get a vision prototype if it is a [SolidReferenceGroup] or vision itself if it is not */ public val Vision.prototype: Vision - get() = when (this) { - is AbstractReference -> prototype - else -> this - } + get() = if (this is SolidReference) prototype else this /** * Create ref for existing prototype @@ -134,16 +163,16 @@ public val Vision.prototype: Vision public fun SolidGroup.ref( templateName: Name, name: String = "", -): SolidReference = SolidReference(templateName).also { set(name, it) } +): SolidReferenceGroup = SolidReferenceGroup(templateName).also { set(name, it) } /** - * Add new [SolidReference] wrapping given object and automatically adding it to the prototypes + * Add new [SolidReferenceGroup] wrapping given object and automatically adding it to the prototypes */ public fun SolidGroup.ref( name: String, obj: Solid, templateName: Name = name.toName(), -): SolidReference { +): SolidReferenceGroup { val existing = getPrototype(templateName) if (existing == null) { prototypes { diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Axes.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Axes.kt index 5e835c83..802bebd6 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Axes.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Axes.kt @@ -1,9 +1,12 @@ package hep.dataforge.vision.solid.specifications -import hep.dataforge.meta.* +import hep.dataforge.meta.Scheme +import hep.dataforge.meta.SchemeSpec +import hep.dataforge.meta.boolean +import hep.dataforge.meta.double public class Axes : Scheme() { - public var visible: Boolean by boolean(!config.isEmpty()) + public var visible: Boolean by boolean(false) public var size: Double by double(AXIS_SIZE) public var width: Double by double(AXIS_WIDTH) diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt index b62ea275..2a147100 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt @@ -4,9 +4,10 @@ import hep.dataforge.meta.* import hep.dataforge.names.Name public class Canvas3DOptions : Scheme() { - public var axes: Axes by spec(Axes, Axes.empty()) - public var camera: Camera by spec(Camera, Camera.empty()) - public var controls: Controls by spec(Controls, Controls.empty()) + public var axes: Axes by spec(Axes) + public var light: Light by spec(Light) + public var camera: Camera by spec(Camera) + public var controls: Controls by spec(Controls) public var minSize: Int by int(400) public var minWith: Number by number { minSize } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Light.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Light.kt new file mode 100644 index 00000000..46ba8939 --- /dev/null +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Light.kt @@ -0,0 +1,8 @@ +package hep.dataforge.vision.solid.specifications + +import hep.dataforge.meta.Scheme +import hep.dataforge.meta.SchemeSpec + +public class Light : Scheme() { + public companion object : SchemeSpec(::Light) +} \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/RemoveSingleChild.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/RemoveSingleChild.kt index ff879d7e..0b69d23b 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/RemoveSingleChild.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/RemoveSingleChild.kt @@ -1,18 +1,15 @@ package hep.dataforge.vision.solid.transform import hep.dataforge.meta.DFExperimental -import hep.dataforge.meta.update import hep.dataforge.names.asName -import hep.dataforge.vision.MutableVisionGroup -import hep.dataforge.vision.Vision -import hep.dataforge.vision.VisionGroup +import hep.dataforge.vision.* import hep.dataforge.vision.solid.* @DFExperimental internal fun mergeChild(parent: VisionGroup, child: Vision): Vision { return child.apply { - config.update(parent.config) + configure(parent.meta) //parent.properties?.let { config.update(it) } @@ -40,7 +37,7 @@ internal object RemoveSingleChild : VisualTreeTransform() { override fun SolidGroup.transformInPlace() { fun MutableVisionGroup.replaceChildren() { children.forEach { (childName, parent) -> - if (parent is SolidReference) return@forEach //ignore refs + if (parent is SolidReferenceGroup) return@forEach //ignore refs if (parent is MutableVisionGroup) { parent.replaceChildren() } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/UnRef.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/UnRef.kt index d45c2ca2..8bc81ff1 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/UnRef.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/UnRef.kt @@ -6,7 +6,7 @@ import hep.dataforge.names.asName import hep.dataforge.vision.MutableVisionGroup import hep.dataforge.vision.VisionGroup import hep.dataforge.vision.solid.SolidGroup -import hep.dataforge.vision.solid.SolidReference +import hep.dataforge.vision.solid.SolidReferenceGroup @DFExperimental internal object UnRef : VisualTreeTransform() { @@ -17,7 +17,7 @@ internal object UnRef : VisualTreeTransform() { counter.forEach { (key, value) -> reducer[key] = (reducer[key] ?: 0) + value } - } else if (obj is SolidReference) { + } else if (obj is SolidReferenceGroup) { reducer[obj.templateName] = (reducer[obj.templateName] ?: 0) + 1 } @@ -26,9 +26,11 @@ internal object UnRef : VisualTreeTransform() { } private fun MutableVisionGroup.unref(name: Name) { - (this as? SolidGroup)?.prototypes?.set(name, null) - children.filter { (it.value as? SolidReference)?.templateName == name }.forEach { (key, value) -> - val reference = value as SolidReference + (this as? SolidGroup)?.prototypes{ + set(name, null) + } + children.filter { (it.value as? SolidReferenceGroup)?.templateName == name }.forEach { (key, value) -> + val reference = value as SolidReferenceGroup val newChild = mergeChild(reference, reference.prototype) newChild.parent = null set(key.asName(), newChild) // replace proxy with merged object diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/CompositeTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/CompositeTest.kt new file mode 100644 index 00000000..45cc6d49 --- /dev/null +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/CompositeTest.kt @@ -0,0 +1,29 @@ +package hep.dataforge.vision.solid + +import hep.dataforge.vision.Colors +import kotlin.test.Test +import kotlin.test.assertEquals + +class CompositeTest { + + @Test + fun testCompositeBuilder(){ + lateinit var composite: Composite + SolidGroup { + composite = composite(CompositeType.INTERSECT) { + y = 300 + box(100, 100, 100) { + z = 50 + } + sphere(50) { + detail = 32 + } + material { + color("pink") + } + } + } + + assertEquals("pink", composite.color.string) + } +} \ No newline at end of file diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/ConvexTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/ConvexTest.kt index d2dbc4b3..912e3b5a 100644 --- a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/ConvexTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/ConvexTest.kt @@ -28,7 +28,7 @@ class ConvexTest { val json = SolidManager.jsonForSolids.encodeToJsonElement(Convex.serializer(), convex) val meta = json.toMetaItem().node!! - val points = meta.getIndexed("points").values.map { (it as MetaItem.NodeItem<*>).node.point3D() } + val points = meta.getIndexed("points").values.map { (it as NodeItem<*>).node.point3D() } assertEquals(8, points.count()) assertEquals(8, convex.points.size) diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/PropertyTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/PropertyTest.kt index 907edf3e..5f6fad96 100644 --- a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/PropertyTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/PropertyTest.kt @@ -1,10 +1,8 @@ package hep.dataforge.vision.solid import hep.dataforge.meta.int -import hep.dataforge.meta.set import hep.dataforge.names.asName -import hep.dataforge.vision.styleSheet -import hep.dataforge.vision.useStyle +import hep.dataforge.vision.* import kotlin.test.Test import kotlin.test.assertEquals @@ -14,12 +12,12 @@ class PropertyTest { fun testInheritedProperty() { var box: Box? = null val group = SolidGroup().apply { - config["test"] = 22 + setProperty("test", 22) group { box = box(100, 100, 100) } } - assertEquals(22, box?.getProperty("test".asName()).int) + assertEquals(22, box?.getProperty("test", inherit = true).int) } @Test @@ -37,7 +35,7 @@ class PropertyTest { } } } - assertEquals(22, box?.getProperty("test".asName()).int) + assertEquals(22, box?.getProperty("test").int) } @Test @@ -60,7 +58,7 @@ class PropertyTest { @Test fun testReferenceStyleProperty() { - var box: SolidReference? = null + var box: SolidReferenceGroup? = null val group = SolidGroup{ styleSheet { set("testStyle") { diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SerializationTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SerializationTest.kt index efef7fcd..10531796 100644 --- a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SerializationTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SerializationTest.kt @@ -15,7 +15,7 @@ fun SolidGroup.refGroup( name: String, templateName: Name = name.toName(), block: MutableVisionGroup.() -> Unit -): SolidReference { +): SolidReferenceGroup { val group = SolidGroup().apply(block) return ref(name, group, templateName) } @@ -32,7 +32,7 @@ class SerializationTest { val string = SolidManager.encodeToString(cube) println(string) val newCube = SolidManager.decodeFromString(string) - assertEquals(cube.config, newCube.config) + assertEquals(cube.meta, newCube.meta) } @Test @@ -53,7 +53,7 @@ class SerializationTest { val string = SolidManager.encodeToString(group) println(string) val reconstructed = SolidManager.decodeFromString(string) as SolidGroup - assertEquals(group["cube"]?.config, reconstructed["cube"]?.config) + assertEquals(group["cube"]?.meta, reconstructed["cube"]?.meta) } } \ No newline at end of file diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SolidReferenceTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SolidReferenceTest.kt new file mode 100644 index 00000000..8b3c85e0 --- /dev/null +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SolidReferenceTest.kt @@ -0,0 +1,26 @@ +package hep.dataforge.vision.solid + +import hep.dataforge.vision.get +import hep.dataforge.vision.style +import hep.dataforge.vision.styles +import hep.dataforge.vision.useStyle +import kotlin.test.Test +import kotlin.test.assertEquals + +class SolidReferenceTest { + val groupWithReference = SolidGroup { + val referenceStyle by style { + SolidMaterial.MATERIAL_COLOR_KEY put "red" + } + ref("test", Box(100f,100f,100f).apply { + color("blue") + useStyle(referenceStyle) + }) + } + + + @Test + fun testReferenceProperty(){ + assertEquals("blue", (groupWithReference["test"] as Solid).color.string) + } +} \ No newline at end of file diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/VisionUpdateTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/VisionUpdateTest.kt index f5ff4f14..17babd41 100644 --- a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/VisionUpdateTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/VisionUpdateTest.kt @@ -29,7 +29,7 @@ class VisionUpdateTest { targetVision.update(dif) assertTrue { targetVision["top"] is SolidGroup } assertEquals("red", (targetVision["origin"] as Solid).color.string) // Should work - assertEquals("#00007b", (targetVision["top"] as SolidGroup).color.string) // new item always takes precedence + assertEquals("#00007b", (targetVision["top"] as Solid).color.string) // new item always takes precedence } @Test @@ -45,7 +45,7 @@ class VisionUpdateTest { val serialized = visionManager.jsonFormat.encodeToString(VisionChange.serializer(), change) println(serialized) val reconstructed = visionManager.jsonFormat.decodeFromString(VisionChange.serializer(), serialized) - assertEquals(change.propertyChange,reconstructed.propertyChange) + assertEquals(change.properties,reconstructed.properties) } @Test diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/MeshThreeFactory.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/MeshThreeFactory.kt index 44478c61..f04e7a1a 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/MeshThreeFactory.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/MeshThreeFactory.kt @@ -9,10 +9,8 @@ import hep.dataforge.names.startsWith import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.SolidMaterial import hep.dataforge.vision.solid.layer -import hep.dataforge.vision.solid.three.ThreeMaterials.getMaterial import info.laht.threekt.core.BufferGeometry import info.laht.threekt.geometries.EdgesGeometry -import info.laht.threekt.geometries.WireframeGeometry import info.laht.threekt.objects.LineSegments import info.laht.threekt.objects.Mesh import kotlin.reflect.KClass @@ -21,14 +19,14 @@ import kotlin.reflect.KClass * Basic geometry-based factory */ public abstract class MeshThreeFactory( - override val type: KClass + override val type: KClass, ) : ThreeFactory { /** * Build a geometry for an object */ public abstract fun buildGeometry(obj: T): BufferGeometry - override fun invoke(obj: T): Mesh { + override fun invoke(three: ThreePlugin, obj: T): Mesh { val geometry = buildGeometry(obj) //JS sometimes tries to pass Geometry as BufferGeometry @@ -36,58 +34,66 @@ public abstract class MeshThreeFactory( //val meshMeta: Meta = obj.properties[Material3D.MATERIAL_KEY]?.node ?: Meta.empty - val mesh = Mesh(geometry, null).apply{ + val mesh = Mesh(geometry, ThreeMaterials.DEFAULT).apply { matrixAutoUpdate = false //set position for mesh updatePosition(obj) }.applyProperties(obj) //add listener to object properties - obj.onPropertyChange(this) { name -> + obj.onPropertyChange(three.updateScope) { name -> when { name.startsWith(Solid.GEOMETRY_KEY) -> { val oldGeometry = mesh.geometry as BufferGeometry val newGeometry = buildGeometry(obj) oldGeometry.attributes = newGeometry.attributes - mesh.applyWireFrame(obj) + //mesh.applyWireFrame(obj) mesh.applyEdges(obj) newGeometry.dispose() } - name.startsWith(WIREFRAME_KEY) -> mesh.applyWireFrame(obj) + //name.startsWith(WIREFRAME_KEY) -> mesh.applyWireFrame(obj) name.startsWith(EDGES_KEY) -> mesh.applyEdges(obj) else -> mesh.updateProperty(obj, name) } } + return mesh } public companion object { public val EDGES_KEY: Name = "edges".asName() - public val WIREFRAME_KEY: Name = "wireframe".asName() + //public val WIREFRAME_KEY: Name = "wireframe".asName() public val ENABLED_KEY: Name = "enabled".asName() public val EDGES_ENABLED_KEY: Name = EDGES_KEY + ENABLED_KEY public val EDGES_MATERIAL_KEY: Name = EDGES_KEY + SolidMaterial.MATERIAL_KEY - public val WIREFRAME_ENABLED_KEY: Name = WIREFRAME_KEY + ENABLED_KEY - public val WIREFRAME_MATERIAL_KEY: Name = WIREFRAME_KEY + SolidMaterial.MATERIAL_KEY + //public val WIREFRAME_ENABLED_KEY: Name = WIREFRAME_KEY + ENABLED_KEY + //public val WIREFRAME_MATERIAL_KEY: Name = WIREFRAME_KEY + SolidMaterial.MATERIAL_KEY } } -fun Mesh.applyProperties(obj: Solid): Mesh = apply{ - material = getMaterial(obj, true) +internal fun Mesh.applyProperties(obj: Solid): Mesh = apply { + updateMaterial(obj) applyEdges(obj) - applyWireFrame(obj) + //applyWireFrame(obj) layers.enable(obj.layer) children.forEach { it.layers.enable(obj.layer) } } -fun Mesh.applyEdges(obj: Solid) { +public fun Mesh.applyEdges(obj: Solid) { val edges = children.find { it.name == "@edges" } as? LineSegments //inherited edges definition, enabled by default - if (obj.getProperty(MeshThreeFactory.EDGES_ENABLED_KEY).boolean != false) { + if (obj.getProperty(MeshThreeFactory.EDGES_ENABLED_KEY, inherit = true, includeStyles = true).boolean != false) { val bufferGeometry = geometry as? BufferGeometry ?: return - val material = ThreeMaterials.getLineMaterial(obj.getProperty(MeshThreeFactory.EDGES_MATERIAL_KEY).node, true) + val material = ThreeMaterials.getLineMaterial( + obj.getProperty( + MeshThreeFactory.EDGES_MATERIAL_KEY, + inherit = true, + includeStyles = true + ).node, + true + ) if (edges == null) { add( LineSegments( @@ -108,22 +114,23 @@ fun Mesh.applyEdges(obj: Solid) { } } -fun Mesh.applyWireFrame(obj: Solid) { - children.find { it.name == "@wireframe" }?.let { - remove(it) - (it as LineSegments).dispose() - } - //inherited wireframe definition, disabled by default - if (obj.getProperty(MeshThreeFactory.WIREFRAME_ENABLED_KEY).boolean == true) { - val bufferGeometry = geometry as? BufferGeometry ?: return - val material = ThreeMaterials.getLineMaterial(obj.getProperty(MeshThreeFactory.WIREFRAME_MATERIAL_KEY).node, true) - add( - LineSegments( - WireframeGeometry(bufferGeometry), - material - ).apply { - name = "@wireframe" - } - ) - } -} \ No newline at end of file +//public fun Mesh.applyWireFrame(obj: Solid) { +// children.find { it.name == "@wireframe" }?.let { +// remove(it) +// (it as LineSegments).dispose() +// } +// //inherited wireframe definition, disabled by default +// if (obj.getProperty(MeshThreeFactory.WIREFRAME_ENABLED_KEY).boolean == true) { +// val bufferGeometry = geometry as? BufferGeometry ?: return +// val material = +// ThreeMaterials.getLineMaterial(obj.getProperty(MeshThreeFactory.WIREFRAME_MATERIAL_KEY).node, true) +// add( +// LineSegments( +// WireframeGeometry(bufferGeometry), +// material +// ).apply { +// name = "@wireframe" +// } +// ) +// } +//} \ No newline at end of file diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeCanvas.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeCanvas.kt index 11fffe88..6df44691 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeCanvas.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeCanvas.kt @@ -1,6 +1,6 @@ package hep.dataforge.vision.solid.three -import hep.dataforge.meta.getItem +import hep.dataforge.meta.get import hep.dataforge.meta.string import hep.dataforge.names.Name import hep.dataforge.names.plus @@ -20,6 +20,7 @@ import info.laht.threekt.external.controls.OrbitControls import info.laht.threekt.external.controls.TrackballControls import info.laht.threekt.geometries.EdgesGeometry import info.laht.threekt.helpers.AxesHelper +import info.laht.threekt.lights.AmbientLight import info.laht.threekt.materials.LineBasicMaterial import info.laht.threekt.math.Vector2 import info.laht.threekt.objects.LineSegments @@ -50,8 +51,11 @@ public class ThreeCanvas( public var axes: AxesHelper = AxesHelper(options.axes.size.toInt()).apply { visible = options.axes.visible } private set + private var light = buildLight(options.light) + private val scene: Scene = Scene().apply { add(axes) + add(light) } public var camera: PerspectiveCamera = buildCamera(options.camera) @@ -66,6 +70,7 @@ public class ThreeCanvas( } private val canvas = (renderer.domElement as HTMLCanvasElement).apply { + className += "three-canvas" width = 600 height = 600 style.apply { @@ -130,7 +135,8 @@ public class ThreeCanvas( } } - public fun attach(element: Element) { + internal fun attach(element: Element) { + check(element.getElementsByClassName("three-canvas").length == 0){"Three canvas already created in this element"} element.appendChild(canvas) updateSize() } @@ -163,6 +169,7 @@ public class ThreeCanvas( } } + private fun buildLight(spec: Light): info.laht.threekt.lights.Light = AmbientLight(0x404040) private fun buildCamera(spec: Camera) = PerspectiveCamera( spec.fov, @@ -176,7 +183,7 @@ public class ThreeCanvas( } private fun addControls(element: Node, controls: Controls) { - when (controls.getItem("type").string) { + when (controls.get("type").string) { "trackball" -> TrackballControls(camera, element) else -> OrbitControls(camera, element) } @@ -189,8 +196,10 @@ public class ThreeCanvas( } public override fun render(vision: Solid) { - //clear old root - clear() + scene.children.find { it.name == "@root" }?.let { + //Throw error is something is already rendered here + error("Root object already is present in the canvas") + } val object3D = three.buildObject3D(vision) object3D.name = "@root" diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeCanvasLabelFactory.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeCanvasLabelFactory.kt index 90027451..f8d26cec 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeCanvasLabelFactory.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeCanvasLabelFactory.kt @@ -22,7 +22,7 @@ import kotlin.reflect.KClass public object ThreeCanvasLabelFactory : ThreeFactory { override val type: KClass get() = SolidLabel::class - override fun invoke(obj: SolidLabel): Object3D { + override fun invoke(three: ThreePlugin, obj: SolidLabel): Object3D { val canvas = document.createElement("canvas") as HTMLCanvasElement val context = canvas.getContext("2d") as CanvasRenderingContext2D context.font = "Bold ${obj.fontSize}pt ${obj.fontFamily}" diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeConvexFactory.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeConvexFactory.kt index 0f5160f8..e41035c9 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeConvexFactory.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeConvexFactory.kt @@ -2,11 +2,10 @@ package hep.dataforge.vision.solid.three import hep.dataforge.vision.solid.Convex import info.laht.threekt.external.geometries.ConvexBufferGeometry -import info.laht.threekt.math.Vector3 public object ThreeConvexFactory : MeshThreeFactory(Convex::class) { override fun buildGeometry(obj: Convex): ConvexBufferGeometry { - @Suppress("USELESS_CAST") val vectors = obj.points.toTypedArray() as Array + val vectors = obj.points.map { it.toVector() }.toTypedArray() return ConvexBufferGeometry(vectors) } } \ No newline at end of file diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeFactory.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeFactory.kt index d01eed4c..9113dbc8 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeFactory.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeFactory.kt @@ -7,7 +7,6 @@ import hep.dataforge.vision.Vision import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.SolidMaterial.Companion.MATERIAL_KEY import hep.dataforge.vision.solid.three.ThreeFactory.Companion.TYPE -import hep.dataforge.vision.solid.three.ThreeMaterials.getMaterial import hep.dataforge.vision.visible import info.laht.threekt.core.BufferGeometry import info.laht.threekt.core.Object3D @@ -22,7 +21,7 @@ public interface ThreeFactory { public val type: KClass - public operator fun invoke(obj: T): Object3D + public operator fun invoke(three: ThreePlugin, obj: T): Object3D public companion object { public const val TYPE: String = "threeFactory" @@ -34,7 +33,7 @@ public interface ThreeFactory { */ public fun Object3D.updatePosition(obj: Vision) { visible = obj.visible ?: true - if(obj is Solid) { + if (obj is Solid) { position.set(obj.x, obj.y, obj.z) setRotationFromEuler(obj.euler) scale.set(obj.scaleX, obj.scaleY, obj.scaleZ) @@ -47,10 +46,10 @@ public fun Object3D.updatePosition(obj: Vision) { */ public fun Object3D.updateProperty(source: Vision, propertyName: Name) { if (this is Mesh && propertyName.startsWith(MATERIAL_KEY)) { - this.material = getMaterial(source, false) + updateMaterialProperty(source, propertyName) } else if ( propertyName.startsWith(Solid.POSITION_KEY) - || propertyName.startsWith(Solid.ROTATION) + || propertyName.startsWith(Solid.ROTATION_KEY) || propertyName.startsWith(Solid.SCALE_KEY) ) { //update position of mesh using this object diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeLabelFactory.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeLabelFactory.kt index ecea5d23..8809ea2d 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeLabelFactory.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeLabelFactory.kt @@ -1,8 +1,8 @@ package hep.dataforge.vision.solid.three +import hep.dataforge.context.logger import hep.dataforge.vision.solid.SolidLabel -import hep.dataforge.vision.solid.three.ThreeMaterials.getMaterial import info.laht.threekt.core.Object3D import info.laht.threekt.geometries.TextBufferGeometry import info.laht.threekt.objects.Mesh @@ -15,17 +15,19 @@ import kotlin.reflect.KClass public object ThreeLabelFactory : ThreeFactory { override val type: KClass get() = SolidLabel::class - override fun invoke(obj: SolidLabel): Object3D { + override fun invoke(three: ThreePlugin, obj: SolidLabel): Object3D { val textGeo = TextBufferGeometry(obj.text, jsObject { font = obj.fontFamily size = 20 height = 1 curveSegments = 1 }) - return Mesh(textGeo, getMaterial(obj,true)).apply { + return Mesh(textGeo, ThreeMaterials.DEFAULT).apply { + updateMaterial(obj) updatePosition(obj) - obj.onPropertyChange(this@ThreeLabelFactory) { _ -> + obj.onPropertyChange(three.updateScope) { _ -> //TODO + three.logger.warn { "Label parameter change not implemented" } } } } diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeLineFactory.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeLineFactory.kt index b5ec871e..0d7a8e5c 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeLineFactory.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeLineFactory.kt @@ -14,7 +14,7 @@ import kotlin.reflect.KClass public object ThreeLineFactory : ThreeFactory { override val type: KClass get() = PolyLine::class - override fun invoke(obj: PolyLine): Object3D { + override fun invoke(three: ThreePlugin, obj: PolyLine): Object3D { val geometry = Geometry().apply { vertices = Array(obj.points.size) { obj.points[it].toVector() } } @@ -28,7 +28,7 @@ public object ThreeLineFactory : ThreeFactory { updatePosition(obj) //layers.enable(obj.layer) //add listener to object properties - obj.onPropertyChange(this) { propertyName -> + obj.onPropertyChange(three.updateScope) { propertyName -> updateProperty(obj, propertyName) } } diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeMaterials.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeMaterials.kt index f7ec681d..0403fdb9 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeMaterials.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeMaterials.kt @@ -1,23 +1,27 @@ package hep.dataforge.vision.solid.three import hep.dataforge.meta.* +import hep.dataforge.names.Name import hep.dataforge.values.ValueType import hep.dataforge.values.int import hep.dataforge.values.string import hep.dataforge.vision.Colors import hep.dataforge.vision.Vision +import hep.dataforge.vision.allStyles import hep.dataforge.vision.solid.SolidMaterial import info.laht.threekt.materials.LineBasicMaterial import info.laht.threekt.materials.Material import info.laht.threekt.materials.MeshBasicMaterial import info.laht.threekt.materials.MeshPhongMaterial import info.laht.threekt.math.Color +import info.laht.threekt.objects.Mesh public object ThreeMaterials { public val DEFAULT_COLOR: Color = Color(Colors.darkgreen) public val DEFAULT: MeshBasicMaterial = MeshBasicMaterial().apply { color.set(DEFAULT_COLOR) + cached = true } public val DEFAULT_LINE_COLOR: Color = Color(Colors.black) public val DEFAULT_LINE: LineBasicMaterial = LineBasicMaterial().apply { @@ -54,11 +58,15 @@ public object ThreeMaterials { private val materialCache = HashMap() - private fun buildMaterial(meta: Meta): Material { + internal fun buildMaterial(meta: Meta): Material { return if (meta[SolidMaterial.SPECULAR_COLOR_KEY] != null) { MeshPhongMaterial().apply { color = meta[SolidMaterial.COLOR_KEY]?.getColor() ?: DEFAULT_COLOR specular = meta[SolidMaterial.SPECULAR_COLOR_KEY]!!.getColor() + emissive = specular + reflectivity = 1.0 + refractionRatio = 1.0 + shininess = 100.0 opacity = meta[SolidMaterial.OPACITY_KEY]?.double ?: 1.0 transparent = opacity < 1.0 wireframe = meta[SolidMaterial.WIREFRAME_KEY].boolean ?: false @@ -75,29 +83,40 @@ public object ThreeMaterials { } } - public fun getMaterial(vision3D: Vision, cache: Boolean): Material { - val meta = vision3D.getProperty(SolidMaterial.MATERIAL_KEY).node ?: return DEFAULT - return if (cache) { - materialCache.getOrPut(meta) { buildMaterial(meta) } - } else { - buildMaterial(meta) + internal fun cacheMeta(meta: Meta): Material = materialCache.getOrPut(meta) { + buildMaterial(meta).apply { + cached = true } } + +// internal fun getMaterial(vision: Vision, cache: Boolean): Material { +// val meta = vision.getProperty(SolidMaterial.MATERIAL_KEY, inherit = true).node ?: return DEFAULT +// return if (cache) { +// materialCache.getOrPut(meta) { +// buildMaterial(meta).apply { +// cached = true +// } +// } +// } else { +// buildMaterial(meta) +// } +// } + } /** * Infer color based on meta item */ -public fun MetaItem<*>.getColor(): Color { +public fun MetaItem.getColor(): Color { return when (this) { - is MetaItem.ValueItem -> if (this.value.type == ValueType.NUMBER) { + is ValueItem -> if (this.value.type == ValueType.NUMBER) { val int = value.int Color(int) } else { Color(this.value.string) } - is MetaItem.NodeItem -> { + is NodeItem -> { Color( node[Colors.RED_KEY]?.int ?: 0, node[Colors.GREEN_KEY]?.int ?: 0, @@ -107,3 +126,76 @@ public fun MetaItem<*>.getColor(): Color { } } +private var Material.cached: Boolean + get() = userData["cached"] == true + set(value) { + userData["cached"] = value + } + +public fun Mesh.updateMaterial(vision: Vision) { + //val meta = vision.getProperty(SolidMaterial.MATERIAL_KEY, inherit = true).node + val ownMaterialMeta = vision.getOwnProperty(SolidMaterial.MATERIAL_KEY) + val stylesMaterialMeta = vision.allStyles[SolidMaterial.MATERIAL_KEY] + val parentMaterialMeta = vision.parent?.getProperty( + SolidMaterial.MATERIAL_KEY, + inherit = true, + includeStyles = false, + includeDefaults = false + ) + + material = when { + ownMaterialMeta == null && stylesMaterialMeta == null && parentMaterialMeta == null -> { + //use default is not material properties are defined + ThreeMaterials.DEFAULT + } + ownMaterialMeta == null && parentMaterialMeta == null -> { + //If material is style-based, use cached + ThreeMaterials.cacheMeta(stylesMaterialMeta.node ?: Meta.EMPTY) + } + else -> { + vision.getProperty(SolidMaterial.MATERIAL_KEY).node?.let { + ThreeMaterials.buildMaterial(it) + } ?: ThreeMaterials.DEFAULT + } + } +} + +public fun Mesh.updateMaterialProperty(vision: Vision, propertyName: Name) { + if (material.cached || propertyName == SolidMaterial.MATERIAL_KEY) { + //generate a new material since cached material should not be changed + updateMaterial(vision) + } else { + when (propertyName) { + SolidMaterial.MATERIAL_COLOR_KEY -> { + material.asDynamic().color = vision.getProperty( + SolidMaterial.MATERIAL_COLOR_KEY, + inherit = true, + includeStyles = true, + includeDefaults = false + )?.getColor() ?: ThreeMaterials.DEFAULT_COLOR + material.needsUpdate = true + } + SolidMaterial.MATERIAL_OPACITY_KEY -> { + val opacity = vision.getProperty( + SolidMaterial.MATERIAL_OPACITY_KEY, + inherit = true, + includeStyles = true, + includeDefaults = false + ).double ?: 1.0 + material.asDynamic().opacity = opacity + material.transparent = opacity < 1.0 + material.needsUpdate = true + } + SolidMaterial.MATERIAL_WIREFRAME_KEY -> { + material.asDynamic().wireframe = vision.getProperty( + SolidMaterial.MATERIAL_WIREFRAME_KEY, + inherit = true, + includeStyles = true, + includeDefaults = false + ).boolean ?: false + material.needsUpdate = true + } + else -> console.warn("Unrecognized material property: $propertyName") + } + } +} \ No newline at end of file diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreePlugin.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreePlugin.kt index bcf309bb..080a71f7 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreePlugin.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreePlugin.kt @@ -9,6 +9,9 @@ import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.visible import info.laht.threekt.core.Object3D +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.w3c.dom.Element import org.w3c.dom.HTMLElement import kotlin.collections.set @@ -22,7 +25,9 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { private val objectFactories = HashMap, ThreeFactory<*>>() private val compositeFactory = ThreeCompositeFactory(this) - private val refFactory = ThreeReferenceFactory(this) + + //TODO generate a separate supervisor update scope + internal val updateScope: CoroutineScope get() = context init { //Add specialized factories here @@ -41,76 +46,66 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { as ThreeFactory? } - public fun buildObject3D(obj: Solid): Object3D { - return when (obj) { - is ThreeVision -> obj.render() - is SolidReference -> refFactory(obj) - is SolidGroup -> { - val group = ThreeGroup() - obj.children.forEach { (token, child) -> - if (child is Solid && child.ignore != true) { - try { - val object3D = buildObject3D(child) - group[token] = object3D - } catch (ex: Throwable) { - logger.error(ex) { "Failed to render $child" } - ex.printStackTrace() - } - } - } - - group.apply { - updatePosition(obj) - //obj.onChildrenChange() - - obj.onPropertyChange(this) { name -> - if ( - name.startsWith(Solid.POSITION_KEY) || - name.startsWith(Solid.ROTATION) || - name.startsWith(Solid.SCALE_KEY) - ) { - //update position of mesh using this object - updatePosition(obj) - } else if (name == Vision.VISIBLE_KEY) { - visible = obj.visible ?: true - } - } - - obj.onStructureChange(this) { nameToken, _, child -> -// if (name.isEmpty()) { -// logger.error { "Children change with empty name on $group" } -// return@onChildrenChange -// } - -// val parentName = name.cutLast() -// val childName = name.last()!! - - //removing old object - findChild(nameToken.asName())?.let { oldChild -> - oldChild.parent?.remove(oldChild) - } - - //adding new object - if (child != null && child is Solid) { - try { - val object3D = buildObject3D(child) - set(nameToken, object3D) - } catch (ex: Throwable) { - logger.error(ex) { "Failed to render $child" } - } - } + public fun buildObject3D(obj: Solid): Object3D = when (obj) { + is ThreeVision -> obj.render(this) + is SolidReferenceGroup -> ThreeReferenceFactory(this, obj) + is SolidGroup -> { + val group = ThreeGroup() + obj.children.forEach { (token, child) -> + if (child is Solid && child.ignore != true) { + try { + val object3D = buildObject3D(child) + group[token] = object3D + } catch (ex: Throwable) { + logger.error(ex) { "Failed to render $child" } + ex.printStackTrace() } } } - is Composite -> compositeFactory(obj) - else -> { - //find specialized factory for this type if it is present - val factory: ThreeFactory? = findObjectFactory(obj::class) - when { - factory != null -> factory(obj) - obj is GeometrySolid -> ThreeShapeFactory(obj) - else -> error("Renderer for ${obj::class} not found") + + group.apply { + updatePosition(obj) + //obj.onChildrenChange() + + obj.onPropertyChange(updateScope) { name -> + if ( + name.startsWith(Solid.POSITION_KEY) || + name.startsWith(Solid.ROTATION_KEY) || + name.startsWith(Solid.SCALE_KEY) + ) { + //update position of mesh using this object + updatePosition(obj) + } else if (name == Vision.VISIBLE_KEY) { + visible = obj.visible ?: true + } } + + obj.structureChanges.onEach { (nameToken, _, child) -> + //removing old object + findChild(nameToken.asName())?.let { oldChild -> + oldChild.parent?.remove(oldChild) + } + + //adding new object + if (child != null && child is Solid) { + try { + val object3D = buildObject3D(child) + set(nameToken, object3D) + } catch (ex: Throwable) { + logger.error(ex) { "Failed to render $child" } + } + } + }.launchIn(updateScope) + } + } + is Composite -> compositeFactory(this, obj) + else -> { + //find specialized factory for this type if it is present + val factory: ThreeFactory? = findObjectFactory(obj::class) + when { + factory != null -> factory(this, obj) + obj is GeometrySolid -> ThreeShapeFactory(this, obj) + else -> error("Renderer for ${obj::class} not found") } } } @@ -133,14 +128,24 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { return if (vision is Solid) ElementVisionRenderer.DEFAULT_RATING else ElementVisionRenderer.ZERO_RATING } + public fun renderSolid( + element: Element, + vision: Solid, + options: Canvas3DOptions, + ): ThreeCanvas = createCanvas(element, options).apply { + render(vision) + } + override fun render(element: Element, vision: Vision, meta: Meta) { - createCanvas(element, Canvas3DOptions.read(meta)).render( - vision as? Solid ?: error("Solid expected but ${vision::class} is found") + renderSolid( + element, + vision as? Solid ?: error("Solid expected but ${vision::class} is found"), + Canvas3DOptions.read(meta) ) } public companion object : PluginFactory { - override val tag: PluginTag = PluginTag("visual.three", PluginTag.DATAFORGE_GROUP) + override val tag: PluginTag = PluginTag("vision.threejs", PluginTag.DATAFORGE_GROUP) override val type: KClass = ThreePlugin::class override fun invoke(meta: Meta, context: Context): ThreePlugin = ThreePlugin() } @@ -150,7 +155,7 @@ public fun ThreePlugin.render( element: HTMLElement, obj: Solid, options: Canvas3DOptions.() -> Unit = {}, -): ThreeCanvas = createCanvas(element, Canvas3DOptions(options)).apply { render(obj) } +): ThreeCanvas = renderSolid(element, obj, Canvas3DOptions(options)) internal operator fun Object3D.set(token: NameToken, object3D: Object3D) { object3D.name = token.toString() diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeReferenceFactory.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeReferenceFactory.kt index 32ea564f..a1a98572 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeReferenceFactory.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeReferenceFactory.kt @@ -1,20 +1,22 @@ package hep.dataforge.vision.solid.three +import hep.dataforge.meta.node import hep.dataforge.names.cutFirst import hep.dataforge.names.firstOrNull import hep.dataforge.names.toName import hep.dataforge.vision.solid.Solid -import hep.dataforge.vision.solid.SolidReference -import hep.dataforge.vision.solid.SolidReference.Companion.REFERENCE_CHILD_PROPERTY_PREFIX +import hep.dataforge.vision.solid.SolidMaterial +import hep.dataforge.vision.solid.SolidReferenceGroup +import hep.dataforge.vision.solid.SolidReferenceGroup.Companion.REFERENCE_CHILD_PROPERTY_PREFIX import info.laht.threekt.core.BufferGeometry import info.laht.threekt.core.Object3D import info.laht.threekt.objects.Mesh import kotlin.reflect.KClass -public class ThreeReferenceFactory(public val three: ThreePlugin) : ThreeFactory { +public object ThreeReferenceFactory : ThreeFactory { private val cache = HashMap() - override val type: KClass = SolidReference::class + override val type: KClass = SolidReferenceGroup::class private fun Object3D.replicate(): Object3D { return when (this) { @@ -30,7 +32,7 @@ public class ThreeReferenceFactory(public val three: ThreePlugin) : ThreeFactory } } - override fun invoke(obj: SolidReference): Object3D { + override fun invoke(three: ThreePlugin, obj: SolidReferenceGroup): Object3D { val template = obj.prototype val cachedObject = cache.getOrPut(template) { three.buildObject3D(template) @@ -40,10 +42,13 @@ public class ThreeReferenceFactory(public val three: ThreePlugin) : ThreeFactory object3D.updatePosition(obj) if(object3D is Mesh){ + //object3D.material = ThreeMaterials.buildMaterial(obj.getProperty(SolidMaterial.MATERIAL_KEY).node!!) object3D.applyProperties(obj) } - obj.onPropertyChange(this) { name -> + //TODO apply child properties + + obj.onPropertyChange(three.updateScope) { name-> if (name.firstOrNull()?.body == REFERENCE_CHILD_PROPERTY_PREFIX) { val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for reference child property: '$name'") val propertyName = name.cutFirst() @@ -55,6 +60,7 @@ public class ThreeReferenceFactory(public val three: ThreePlugin) : ThreeFactory } } + return object3D } } \ No newline at end of file diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeVision.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeVision.kt index 67870229..8a6d2a52 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeVision.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/ThreeVision.kt @@ -7,5 +7,5 @@ import info.laht.threekt.core.Object3D * A custom visual object that has its own Three.js renderer */ public abstract class ThreeVision : SolidBase() { - public abstract fun render(): Object3D + public abstract fun render(three: ThreePlugin): Object3D } diff --git a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/three.kt b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/three.kt index 862960a9..9a11c550 100644 --- a/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/three.kt +++ b/visionforge-threejs/src/main/kotlin/hep/dataforge/vision/solid/three/three.kt @@ -17,7 +17,7 @@ import kotlin.math.PI public val Solid.euler: Euler get() = Euler(rotationX, rotationY, rotationZ, rotationOrder.name) -public val MetaItem<*>.vector: Vector3 get() = Vector3(node["x"].float ?: 0f, node["y"].float ?: 0f, node["z"].float ?: 0f) +public val MetaItem.vector: Vector3 get() = Vector3(node["x"].float ?: 0f, node["y"].float ?: 0f, node["z"].float ?: 0f) public fun Geometry.toBufferGeometry(): BufferGeometry = BufferGeometry().apply { fromGeometry(this@toBufferGeometry) } diff --git a/visionforge-threejs/src/main/kotlin/info/laht/threekt/helpers/HemisphereLightHelper.kt b/visionforge-threejs/src/main/kotlin/info/laht/threekt/helpers/HemisphereLightHelper.kt index af7333d5..4351faa7 100644 --- a/visionforge-threejs/src/main/kotlin/info/laht/threekt/helpers/HemisphereLightHelper.kt +++ b/visionforge-threejs/src/main/kotlin/info/laht/threekt/helpers/HemisphereLightHelper.kt @@ -28,7 +28,7 @@ package info.laht.threekt.helpers import info.laht.threekt.core.Object3D -import info.laht.threekt.lights.HemiSphereLight +import info.laht.threekt.lights.HemisphereLight import info.laht.threekt.lights.Light /** @@ -39,7 +39,7 @@ import info.laht.threekt.lights.Light * @param color (optional) if this is not the set the helper will take the color of the light. */ external class HemisphereLightHelper( - light: HemiSphereLight, + light: HemisphereLight, size: Number, color: Int = definedExternally ) : Object3D { diff --git a/visionforge-threejs/src/main/kotlin/info/laht/threekt/lights/HemiSphereLight.kt b/visionforge-threejs/src/main/kotlin/info/laht/threekt/lights/HemisphereLight.kt similarity index 94% rename from visionforge-threejs/src/main/kotlin/info/laht/threekt/lights/HemiSphereLight.kt rename to visionforge-threejs/src/main/kotlin/info/laht/threekt/lights/HemisphereLight.kt index 727a36a1..c98fbd5d 100644 --- a/visionforge-threejs/src/main/kotlin/info/laht/threekt/lights/HemiSphereLight.kt +++ b/visionforge-threejs/src/main/kotlin/info/laht/threekt/lights/HemisphereLight.kt @@ -34,7 +34,7 @@ import info.laht.threekt.math.Color * * This light cannot be used to cast shadows. */ -external class HemiSphereLight( +external class HemisphereLight( skyColor: Int = definedExternally, groundColor: Int = definedExternally, intensity: Number = definedExternally @@ -42,6 +42,6 @@ external class HemiSphereLight( var groundColor: Color - fun copy(light: HemiSphereLight): HemiSphereLight + fun copy(light: HemisphereLight): HemisphereLight } \ No newline at end of file diff --git a/visionforge-threejs/src/main/kotlin/info/laht/threekt/materials/Material.kt b/visionforge-threejs/src/main/kotlin/info/laht/threekt/materials/Material.kt index 3950c963..9a2b72d0 100644 --- a/visionforge-threejs/src/main/kotlin/info/laht/threekt/materials/Material.kt +++ b/visionforge-threejs/src/main/kotlin/info/laht/threekt/materials/Material.kt @@ -111,7 +111,7 @@ open external class Material { var visible: Boolean - var userData: Map + var userData: dynamic /** * Specifies that the material needs to be updated at the WebGL level. Set it to true if you made changes that need to be reflected in WebGL. diff --git a/visionforge-threejs/visionforge-threejs-server/src/commonMain/kotlin/hep/dataforge/vision/three/server/visionContext.kt b/visionforge-threejs/visionforge-threejs-server/src/commonMain/kotlin/hep/dataforge/vision/three/server/visionContext.kt deleted file mode 100644 index c568d74a..00000000 --- a/visionforge-threejs/visionforge-threejs-server/src/commonMain/kotlin/hep/dataforge/vision/three/server/visionContext.kt +++ /dev/null @@ -1,8 +0,0 @@ -package hep.dataforge.vision.three.server - -import hep.dataforge.context.Context -import hep.dataforge.vision.VisionManager - -public expect val visionContext: Context - -public val visionManager: VisionManager get() = visionContext.plugins.fetch(VisionManager) \ No newline at end of file diff --git a/visionforge-threejs/visionforge-threejs-server/src/jsMain/kotlin/hep/dataforge/vision/three/server/jsMain.kt b/visionforge-threejs/visionforge-threejs-server/src/jsMain/kotlin/hep/dataforge/vision/three/server/jsMain.kt index f9505680..86ad4ecc 100644 --- a/visionforge-threejs/visionforge-threejs-server/src/jsMain/kotlin/hep/dataforge/vision/three/server/jsMain.kt +++ b/visionforge-threejs/visionforge-threejs-server/src/jsMain/kotlin/hep/dataforge/vision/three/server/jsMain.kt @@ -1,46 +1,18 @@ package hep.dataforge.vision.three.server -import hep.dataforge.context.Context import hep.dataforge.context.Global import hep.dataforge.vision.client.VisionClient import hep.dataforge.vision.client.renderAllVisions import hep.dataforge.vision.solid.three.ThreePlugin import kotlinx.browser.window -//FIXME check plugin loading in JS -//public actual val visionContext: Context = Global.context("vision-client") { -// //Loading three-js renderer -// plugin(ThreePlugin) -//} - -public actual val visionContext: Context = Global.context("vision-client").apply { - //Loading three-js renderer - plugins.fetch(ThreePlugin) -} - -public val clientManager: VisionClient get() = visionContext.plugins.fetch(VisionClient) - - -///** -// * Render all visions in the document using registered renderers -// */ -//@JsExport -//public fun renderVisions() { -// //Fetch from server and render visions for all outputs -// window.onload = { -// clientManager.renderAllVisions() -// } -//} -// -///** -// * Render all visions in a given element, using registered renderers -// */ -//@JsExport -//public fun renderAllVisionsAt(element: Element) { -// clientManager.renderAllVisionsAt(element) -//} - public fun main() { + //Loading three-js renderer + val visionContext = Global.context("threejs") { + plugin(ThreePlugin) + } + val clientManager = visionContext.plugins.fetch(VisionClient) + //Fetch from server and render visions for all outputs window.onload = { clientManager.renderAllVisions() diff --git a/visionforge-threejs/visionforge-threejs-server/src/jvmMain/kotlin/hep/dataforge/vision/three/server/serverExtensions.kt b/visionforge-threejs/visionforge-threejs-server/src/jvmMain/kotlin/hep/dataforge/vision/three/server/serverExtensions.kt index a4d84a23..ca20c709 100644 --- a/visionforge-threejs/visionforge-threejs-server/src/jvmMain/kotlin/hep/dataforge/vision/three/server/serverExtensions.kt +++ b/visionforge-threejs/visionforge-threejs-server/src/jvmMain/kotlin/hep/dataforge/vision/three/server/serverExtensions.kt @@ -1,45 +1,25 @@ package hep.dataforge.vision.three.server import hep.dataforge.context.Context -import hep.dataforge.context.Global import hep.dataforge.meta.DFExperimental import hep.dataforge.vision.ResourceLocation import hep.dataforge.vision.html.HtmlVisionFragment -import hep.dataforge.vision.html.VisionOutput -import hep.dataforge.vision.makeFile +import hep.dataforge.vision.makeVisionFile import hep.dataforge.vision.scriptHeader -import hep.dataforge.vision.solid.SolidGroup -import hep.dataforge.vision.solid.SolidManager -import java.nio.file.Files import java.nio.file.Path -public actual val visionContext: Context = Global.context("vision-server") { - //Loading solid manager for the backend (it does not know about three - plugin(SolidManager) -} public fun VisionServer.useThreeJs(): Unit { useScript("js/visionforge-three.js") -// header { -// script { -// unsafe { -// +"renderThreeVisions()" -// } -// } -// } } @DFExperimental -public inline fun VisionOutput.solid(block: SolidGroup.() -> Unit): SolidGroup = SolidGroup().apply(block) - -@OptIn(DFExperimental::class) -public fun HtmlVisionFragment.makeFile( +public fun Context.makeVisionFile( + fragment: HtmlVisionFragment, path: Path? = null, title: String = "VisionForge page", resourceLocation: ResourceLocation = ResourceLocation.SYSTEM, show: Boolean = true, -) { - val actualPath = path ?: Files.createTempFile("tempPlot", ".html") - val scriptHeader = Context.scriptHeader("/js/visionforge-three.js", actualPath, resourceLocation) - makeFile(visionManager, path = path, show = show, title = title, headers = arrayOf(scriptHeader)) +): Unit = makeVisionFile(fragment, path = path, title = title, show = show) { actualPath -> + scriptHeader("js/visionforge-three.js", actualPath, resourceLocation) } \ No newline at end of file