Merge pull request #34 from mipt-npm/new-properties

New properties
This commit is contained in:
Alexander Nozik 2020-12-29 13:35:01 +03:00 committed by GitHub
commit f0f117d4fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 1881 additions and 1259 deletions

View File

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

View File

@ -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")

View File

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

View File

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

View File

@ -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()

View File

@ -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<MMAppProps>("Muon monitor") { props ->
val root = props.model.root
gridRow{
gridRow {
flexColumn {
css {
+"col-lg-3"
@ -109,8 +109,8 @@ val MMApp = functionalComponent<MMAppProps>("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<MMAppProps>("Muon monitor") { props ->
}
}
}
styledDiv{
css{
styledDiv {
css {
padding(0.px)
}
nav {
@ -180,10 +180,10 @@ val MMApp = functionalComponent<MMAppProps>("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<MMAppProps>("Muon monitor") { props ->
else -> root[selected]
}
if (selectedObject != null) {
configEditor(
selectedObject.config,
selectedObject.descriptor,
default = selectedObject.allProperties,
key = selected
)
visionPropertyEditor(selectedObject, key = selected)
}
}
}

View File

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

View File

@ -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)
}
}

View File

@ -39,15 +39,18 @@ fun Page<Solid>.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<Solid>.showcase() {
val group = group {
box(100, 100, 100) {
z = 110.0
opacity = 0.5
}
box(100, 100, 100) {
@ -143,8 +147,7 @@ fun Page<Solid>.showcaseCSG() {
detail = 32
}
material {
color(Colors.red)
wireframe = false
color(Colors.pink)
}
}
composite(CompositeType.UNION) {
@ -154,8 +157,8 @@ fun Page<Solid>.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<Solid>.showcaseCSG() {
sphere(50){
detail = 32
}
color(Colors.teal)
color("teal")
opacity = 0.7
}
}
@ -173,9 +176,11 @@ fun Page<Solid>.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
}
}
}

View File

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

View File

@ -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<ProcessResources>("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"))
}
}
}

View File

@ -0,0 +1,2 @@

View File

@ -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<String, Any>) {
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<String, Any>) {
//
// 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)
}

View File

@ -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
)
}

View File

@ -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)
}

View File

@ -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)
}

View File

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

View File

@ -51,11 +51,7 @@ public val ThreeControls: FunctionalComponent<ThreeControlsProps> = 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

View File

@ -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)
}

View File

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

View File

@ -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<Name>?
}
private val ConfigEditorItem: FunctionalComponent<ConfigEditorItemProps> =
private val PropertyEditorItem: FunctionalComponent<PropertyEditorProps> =
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<Config>? by useState { props.root[props.name] }
val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name)
val defaultItem = props.default?.get(props.name)
var actualItem: MetaItem<Meta>? 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<ConfigEditorProps> = functionalComponent("ConfigEditor") { props ->
child(ConfigEditorItem) {
public val PropertyEditor: FunctionalComponent<PropertyEditorProps> = 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<Name>? = 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<Name> = 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
}
}
}
scope: CoroutineScope? = null,
): Unit = render(this) {
configEditor(config, default, descriptor, key, scope)
}
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)

View File

@ -37,8 +37,7 @@ public val ThreeCanvasComponent: FunctionalComponent<ThreeCanvasProps> = 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
}

View File

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

View File

@ -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<ValueChooserProps, ValueChooserState>(props) {
private val element = createRef<HTMLElement>()
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<ValueChooserProps> =
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<ValueChooserProps> =
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<ValueChooserProps> =
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<ValueChooserProps> =
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<ValueChooserProps> =
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<ValueChooserProps> = 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

View File

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

View File

@ -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<Any?, StyleReference> =
ReadOnlyProperty { _, property ->
val styleName = property.name
styleSheet.define(styleName, Meta(builder))
StyleReference(this, styleName)
}

View File

@ -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<NameToken, Meta>? 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<Name> =
((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<String>
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<MetaItem<*>> {
public fun Vision.getStyleItems(name: Name): Sequence<MetaItem> {
return styles.asSequence().map {
getStyle(it)
}.map {
@ -103,3 +111,5 @@ public fun Vision.getStyleItems(name: Name): Sequence<MetaItem<*>> {
* Collect all styles for this object in a single laminate
*/
public val Vision.allStyles: Laminate get() = Laminate(styles.mapNotNull(::getStyle))

View File

@ -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<String>
get() = properties[STYLE_KEY]?.stringList ?: emptyList()
set(value) {
config[STYLE_KEY] = value
@DFExperimental
@OptIn(ExperimentalCoroutinesApi::class)
public val propertyChanges: Flow<Name>
get() = callbackFlow<Name> {
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
}
/**
* 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<Any?, MetaItem<*>?> {
// 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)
}
public fun Vision.setProperty(key: String, item: Any?) {
setProperty(key.toName(), MetaItem.of(item))
}

View File

@ -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<String>) {
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<PropertyListener>()
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<String>) {
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<Name> = MutableSharedFlow()
@DFExperimental
override val propertyChanges: Flow<Name> 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)
}
}
}
}
}

View File

@ -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<Vision> {
private val propertyChange = HashMap<Name, Config>()
private val childrenChange = HashMap<Name, Vision?>()
private var reset: Boolean = false
private var vision: Vision? = null
private val propertyChange = Config()
private val children: HashMap<Name, VisionChangeBuilder> = 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<Name, @Serializable(MetaSerializer::class) Meta>,
val childrenChange: Map<Name, Vision?>,
) {
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<Name, VisionChange>? = 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<VisionChange> = 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()
}
}
}
}

View File

@ -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<out V : Vision> {
public operator fun get(name: Name): V?
@ -44,16 +45,6 @@ public interface VisionGroup : Provider, Vision, VisionContainer<Vision> {
}
}
/**
* 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<in V : Vision> {
*/
public interface MutableVisionGroup : VisionGroup, VisionContainerBuilder<Vision> {
/**
* 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<StructureChange>
}
public operator fun <V : Vision> VisionContainer<V>.get(str: String): V? = get(str.toName())

View File

@ -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<NameToken, Vision> = LinkedHashMap()
public open class VisionGroupBase(
@SerialName("children") internal val childrenInternal: MutableMap<NameToken, Vision> = LinkedHashMap(),
) : VisionBase(), MutableVisionGroup {
/**
* A map of top level named children
*/
override val children: Map<NameToken, Vision> 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<StructureChangeListener>()
private val _structureChanges: MutableSharedFlow<MutableVisionGroup.StructureChange> = 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<MutableVisionGroup.StructureChange> 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)
}

View File

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

View File

@ -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<out T> {
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)
}

View File

@ -73,7 +73,7 @@ public abstract class VisionTagConsumer<R>(
@OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision(
name: String,
name: String = DEFAULT_VISION_NAME,
visionProvider: VisionOutput.() -> Vision,
): T = vision(name.toName(), visionProvider)
@ -103,5 +103,7 @@ public abstract class VisionTagConsumer<R>(
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"
}
}

View File

@ -18,9 +18,10 @@ public fun FlowContent.embedVisionFragment(
val consumer = object : VisionTagConsumer<Any?>(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"
}
}
}

View File

@ -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<MetaItem<*>?>.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<MetaItem?>.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 <reified E : Enum<E>> NodeDescriptor.enum(key: Name, default: E?): Unit = value(key) {
type(ValueType.STRING)
default?.let {
default(default)
}
allowedValues = enumValues<E>().map { it.asValue() }
}
/**
* 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))

View File

@ -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)
}
}

View File

@ -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 <reified E : Enum<E>> NodeDescriptor.enum(
key: Name,
default: E?,
crossinline modifier: ValueDescriptor.() -> Unit = {},
): Unit = value(key) {
type(ValueType.STRING)
default?.let {
default(default)
}
allowedValues = enumValues<E>().map { it.asValue() }
modifier()
}

View File

@ -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() }

View File

@ -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")
}

View File

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

View File

@ -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()

View File

@ -18,7 +18,7 @@ import tornadofx.*
/**
* A display for meta and descriptor
*/
sealed class FXMeta<M : MetaNode<M>> : Comparable<FXMeta<*>> {
sealed class FXMeta<M : TypedMeta<M>> : Comparable<FXMeta<*>> {
abstract val name: NameToken
abstract val parent: FXMetaNode<M>?
abstract val descriptionProperty: ObservableStringValue
@ -35,7 +35,7 @@ sealed class FXMeta<M : MetaNode<M>> : Comparable<FXMeta<*>> {
}
companion object {
fun <M : MetaNode<M>> root(
fun <M : TypedMeta<M>> root(
node: M,
descriptor: NodeDescriptor? = null,
rootName: String = "root"
@ -47,7 +47,7 @@ sealed class FXMeta<M : MetaNode<M>> : Comparable<FXMeta<*>> {
}
}
class FXMetaNode<M : MetaNode<M>>(
class FXMetaNode<M : TypedMeta<M>>(
override val name: NameToken,
override val parent: FXMetaNode<M>?,
nodeValue: M? = null,
@ -89,7 +89,7 @@ class FXMetaNode<M : MetaNode<M>>(
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<M : MetaNode<M>>(
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<M : MetaNode<M>>(
}
}
public class FXMetaValue<M : MetaNode<M>>(
public class FXMetaValue<M : TypedMeta<M>>(
override val name: NameToken,
override val parent: FXMetaNode<M>
) : FXMeta<M>() {
@ -151,10 +151,10 @@ public class FXMetaValue<M : MetaNode<M>>(
//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 <M : MutableMeta<M>> FXMetaNode<M>.remove(name: NameToken) {
private fun <M : MutableMeta<M>> 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!!
}

View File

@ -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)
}

View File

@ -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<FX3DPlugin> {
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()
}

View File

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

View File

@ -6,15 +6,15 @@ import javafx.scene.Group
import javafx.scene.Node
import kotlin.reflect.KClass
class FXReferenceFactory(val plugin: FX3DPlugin) : FX3DFactory<SolidReference> {
override val type: KClass<in SolidReference> get() = SolidReference::class
class FXReferenceFactory(val plugin: FX3DPlugin) : FX3DFactory<SolidReferenceGroup> {
override val type: KClass<in SolidReferenceGroup> 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")

View File

@ -12,11 +12,11 @@ import tornadofx.*
/**
* A caching binding collection for [Vision] properties
*/
class VisualObjectFXBinding(val obj: Vision) {
private val bindings = HashMap<Name, ObjectBinding<MetaItem<*>?>>()
class VisualObjectFXBinding(val fx: FX3DPlugin, val obj: Vision) {
private val bindings = HashMap<Name, ObjectBinding<MetaItem?>>()
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<MetaItem<*>?> {
operator fun get(key: Name): ObjectBinding<MetaItem?> {
return bindings.getOrPut(key) {
object : ObjectBinding<MetaItem<*>?>() {
override fun computeValue(): MetaItem<*>? = obj.getProperty(key)
object : ObjectBinding<MetaItem?>() {
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<MetaItem<*>?>.value() = objectBinding { it.value }
fun ObjectBinding<MetaItem<*>?>.string() = stringBinding { it.string }
fun ObjectBinding<MetaItem<*>?>.number() = objectBinding { it.number }
fun ObjectBinding<MetaItem<*>?>.double() = objectBinding { it.double }
fun ObjectBinding<MetaItem<*>?>.float() = objectBinding { it.float }
fun ObjectBinding<MetaItem<*>?>.int() = objectBinding { it.int }
fun ObjectBinding<MetaItem<*>?>.long() = objectBinding { it.long }
fun ObjectBinding<MetaItem<*>?>.node() = objectBinding { it.node }
fun ObjectBinding<MetaItem?>.value() = objectBinding { it.value }
fun ObjectBinding<MetaItem?>.string() = stringBinding { it.string }
fun ObjectBinding<MetaItem?>.number() = objectBinding { it.number }
fun ObjectBinding<MetaItem?>.double() = objectBinding { it.double }
fun ObjectBinding<MetaItem?>.float() = objectBinding { it.float }
fun ObjectBinding<MetaItem?>.int() = objectBinding { it.int }
fun ObjectBinding<MetaItem?>.long() = objectBinding { it.long }
fun ObjectBinding<MetaItem?>.node() = objectBinding { it.node }
fun ObjectBinding<MetaItem<*>?>.string(default: String) = stringBinding { it.string ?: default }
fun ObjectBinding<MetaItem<*>?>.double(default: Double) = doubleBinding { it.double ?: default }
fun ObjectBinding<MetaItem<*>?>.float(default: Float) = floatBinding { it.float ?: default }
fun ObjectBinding<MetaItem<*>?>.int(default: Int) = integerBinding { it.int ?: default }
fun ObjectBinding<MetaItem<*>?>.long(default: Long) = longBinding { it.long ?: default }
fun ObjectBinding<MetaItem?>.string(default: String) = stringBinding { it.string ?: default }
fun ObjectBinding<MetaItem?>.double(default: Double) = doubleBinding { it.double ?: default }
fun ObjectBinding<MetaItem?>.float(default: Float) = floatBinding { it.float ?: default }
fun ObjectBinding<MetaItem?>.int(default: Int) = integerBinding { it.int ?: default }
fun ObjectBinding<MetaItem?>.long(default: Long) = longBinding { it.long ?: default }
fun <T> ObjectBinding<MetaItem<*>?>.transform(transform: (MetaItem<*>) -> T) = objectBinding { it?.let(transform) }
fun <T> ObjectBinding<MetaItem?>.transform(transform: (MetaItem) -> T) = objectBinding { it?.let(transform) }

View File

@ -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<Name, MutableList<SolidReference>>()
private val referenceStore = HashMap<Name, MutableList<SolidReferenceGroup>>()
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

View File

@ -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)
}
}
}

View File

@ -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}")
}
}
}
}

View File

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

View File

@ -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<PlotlyPlugin> {
override val tag: PluginTag = PluginTag("vision.plotly", PluginTag.DATAFORGE_GROUP)
override val type: KClass<PlotlyPlugin> = PlotlyPlugin::class
override fun invoke(meta: Meta, context: Context): PlotlyPlugin = PlotlyPlugin()
}
}

View File

@ -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()
// }
//}

View File

@ -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<Any?>(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
)

View File

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

View File

@ -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<Solid>.composite(
type: CompositeType,
name: String = "",
builder: SolidGroup.() -> Unit
builder: SolidGroup.() -> Unit,
): Composite {
val group = SolidGroup().apply(builder)
val children = group.children.values.filterIsInstance<Solid>()
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<Solid>.subtract(name: String = "", buil
composite(CompositeType.SUBTRACT, name, builder = builder)
@VisionBuilder
public inline fun VisionContainerBuilder<Solid>.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite =
public inline fun VisionContainerBuilder<Solid>.intersect(
name: String = "",
builder: SolidGroup.() -> Unit,
): Composite =
composite(CompositeType.INTERSECT, name, builder = builder)

View File

@ -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<Point3D>) : 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()

View File

@ -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)
}

View File

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

View File

@ -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<Solid>.() -> 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<Solid>.() -> Unit): Unit {
override fun prototypes(builder: VisionContainerBuilder<Solid>.() -> 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<Vision>.group(name: Name = Name.EMPTY, action: SolidGroup.() -> Unit = {}): SolidGroup =
public fun VisionContainerBuilder<Vision>.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<Vision>.group(name: String, action: SolidGroup.() -> Unit = {}): SolidGroup =
SolidGroup().apply(action).also { set(name, it) }
@ -102,27 +98,30 @@ public fun VisionContainerBuilder<Vision>.group(name: String, action: SolidGroup
@Serializable(Prototypes.Companion::class)
internal class Prototypes(
children: Map<NameToken, Vision> = emptyMap(),
) : VisionGroupBase(), PrototypeHolder {
) : VisionGroupBase(children as? MutableMap<NameToken, Vision> ?: 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<MutableVisionGroup> {
private val mapSerializer: KSerializer<Map<NameToken, Vision>> =
@ -142,4 +141,10 @@ internal class Prototypes(
mapSerializer.serialize(encoder, value.children)
}
}
override fun prototypes(builder: VisionContainerBuilder<Solid>.() -> Unit) {
apply(builder)
}
override fun getPrototype(name: Name): Solid? = get(name) as? Solid
}

View File

@ -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<SolidManager> {
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<out SolidManager> = SolidManager::class
override fun invoke(meta: Meta, context: Context): SolidManager = SolidManager(meta)
private fun PolymorphicModuleBuilder<Solid>.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)

View File

@ -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())
}

View File

@ -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<String>
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<Name, Config> = HashMap()
override val children: Map<NameToken, SolidReference.ReferenceChild>
override val children: Map<NameToken, Vision>
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<NameToken, Vision>
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 {

View File

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

View File

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

View File

@ -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>(::Light)
}

View File

@ -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<SolidGroup>() {
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()
}

View File

@ -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<SolidGroup>() {
@ -17,7 +17,7 @@ internal object UnRef : VisualTreeTransform<SolidGroup>() {
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<SolidGroup>() {
}
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

View File

@ -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)
}
}

View File

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

View File

@ -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") {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

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

View File

@ -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<in T : Solid>(
override val type: KClass<in T>
override val type: KClass<in T>,
) : ThreeFactory<T> {
/**
* 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<in T : Solid>(
//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"
}
)
}
}
//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"
// }
// )
// }
//}

View File

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

View File

@ -22,7 +22,7 @@ import kotlin.reflect.KClass
public object ThreeCanvasLabelFactory : ThreeFactory<SolidLabel> {
override val type: KClass<in SolidLabel> 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}"

View File

@ -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>(Convex::class) {
override fun buildGeometry(obj: Convex): ConvexBufferGeometry {
@Suppress("USELESS_CAST") val vectors = obj.points.toTypedArray() as Array<Vector3>
val vectors = obj.points.map { it.toVector() }.toTypedArray()
return ConvexBufferGeometry(vectors)
}
}

View File

@ -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<in T : Vision> {
public val type: KClass<in T>
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<in T : Vision> {
*/
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

View File

@ -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<SolidLabel> {
override val type: KClass<in SolidLabel> 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" }
}
}
}

View File

@ -14,7 +14,7 @@ import kotlin.reflect.KClass
public object ThreeLineFactory : ThreeFactory<PolyLine> {
override val type: KClass<PolyLine> 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<PolyLine> {
updatePosition(obj)
//layers.enable(obj.layer)
//add listener to object properties
obj.onPropertyChange(this) { propertyName ->
obj.onPropertyChange(three.updateScope) { propertyName ->
updateProperty(obj, propertyName)
}
}

View File

@ -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<Meta, Material>()
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")
}
}
}

View File

@ -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<KClass<out Solid>, 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<Solid>?
}
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<Solid>? = 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<Solid>? = 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<ThreePlugin> {
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> = 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()

View File

@ -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<SolidReference> {
public object ThreeReferenceFactory : ThreeFactory<SolidReferenceGroup> {
private val cache = HashMap<Solid, Object3D>()
override val type: KClass<SolidReference> = SolidReference::class
override val type: KClass<SolidReferenceGroup> = 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
}
}

View File

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

View File

@ -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) }

View File

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

View File

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

View File

@ -111,7 +111,7 @@ open external class Material {
var visible: Boolean
var userData: Map<String, Any>
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.

View File

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

View File

@ -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()

View File

@ -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)
}