diff --git a/README.md b/README.md index 45cd88af..156e2ae3 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ also referred to as templates). The idea is that prototype geometry can be rende for multiple objects. This helps to significantly decrease memory usage. The `prototypes` property tree is defined in `SolidGroup` class via `PrototypeHolder` interface, and -`Proxy` class helps to reuse a template object. +`SolidReference` class helps to reuse a template object. ##### Styles diff --git a/build.gradle.kts b/build.gradle.kts index b793c216..562b2092 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,10 @@ plugins { val dataforgeVersion by extra("0.2.0-dev-7") val ktorVersion by extra("1.4.2") +val htmlVersion by extra("0.7.2") val kotlinWrappersVersion by extra("pre.129-kotlin-1.4.10") +val fxVersion by extra("14") + allprojects { repositories { @@ -21,7 +24,6 @@ allprojects { val githubProject by extra("visionforge") val bintrayRepo by extra("dataforge") -val fxVersion by extra("14") subprojects { if(name.startsWith("visionforge")) { diff --git a/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/ThreeDemoGrid.kt b/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/ThreeDemoGrid.kt index d8420d6f..8ea0462d 100644 --- a/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/ThreeDemoGrid.kt +++ b/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/ThreeDemoGrid.kt @@ -5,12 +5,11 @@ import hep.dataforge.meta.Meta import hep.dataforge.meta.get import hep.dataforge.meta.string import hep.dataforge.names.Name -import hep.dataforge.output.OutputManager import hep.dataforge.output.Renderer import hep.dataforge.vision.Vision import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreePlugin -import hep.dataforge.vision.solid.three.output +import hep.dataforge.vision.solid.three.attachRenderer import kotlinx.browser.document import kotlinx.dom.clear import kotlinx.html.dom.append @@ -23,7 +22,7 @@ import kotlinx.html.span import org.w3c.dom.Element import kotlin.reflect.KClass -class ThreeDemoGrid(element: Element, meta: Meta = Meta.EMPTY) : OutputManager { +class ThreeDemoGrid(element: Element, meta: Meta = Meta.EMPTY) { private val gridRoot = document.create.div("row") private val outputs: MutableMap = HashMap() @@ -46,7 +45,7 @@ class ThreeDemoGrid(element: Element, meta: Meta = Meta.EMPTY) : OutputManager { span("border") { div("col-6") { div { id = "output-$name" }.also { - output = three.output(it, canvasOptions) + output = three.attachRenderer(it, canvasOptions) //output.attach(it) } hr() diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/outputConfig.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/outputConfig.kt index 3a64ef7f..9c17a6cd 100644 --- a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/outputConfig.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/outputConfig.kt @@ -3,7 +3,6 @@ package hep.dataforge.vision.bootstrap import hep.dataforge.vision.react.flexColumn import hep.dataforge.vision.react.flexRow import hep.dataforge.vision.solid.SolidGroup -import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.three.ThreeCanvas import kotlinx.css.* import kotlinx.css.properties.border @@ -45,7 +44,7 @@ public external interface CanvasControlsProps : RProps { public val CanvasControls: FunctionalComponent = functionalComponent("CanvasControls") { props -> val visionManager = useMemo( - { props.canvas.context.plugins.fetch(SolidManager).visionManager }, + { props.canvas.three.solidManager.visionManager }, arrayOf(props.canvas) ) flexColumn { diff --git a/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt b/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt index da424bab..df52938d 100644 --- a/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt @@ -7,7 +7,6 @@ import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreePlugin -import hep.dataforge.vision.solid.three.output import kotlinx.css.* import org.w3c.dom.Element import org.w3c.dom.HTMLElement @@ -40,7 +39,7 @@ public val ThreeCanvasComponent: FunctionalComponent = functio val element = elementRef.current as? HTMLElement ?: error("Canvas element not found") val three: ThreePlugin = props.context.plugins.fetch(ThreePlugin) val newCanvas: ThreeCanvas = - three.output(element, props.options ?: Canvas3DOptions.empty(), props.clickCallback) + three.attachRenderer(element, props.options ?: Canvas3DOptions.empty(), props.clickCallback) props.canvasCallback?.invoke(newCanvas) canvas = newCanvas } diff --git a/visionforge-core/build.gradle.kts b/visionforge-core/build.gradle.kts index 6eee3f0c..74ac090a 100644 --- a/visionforge-core/build.gradle.kts +++ b/visionforge-core/build.gradle.kts @@ -4,13 +4,15 @@ plugins { val dataforgeVersion: String by rootProject.extra val kotlinWrappersVersion: String by rootProject.extra +val htmlVersion: String by rootProject.extra kotlin { sourceSets { commonMain { dependencies { - api("hep.dataforge:dataforge-output:$dataforgeVersion") + api("hep.dataforge:dataforge-context:$dataforgeVersion") + api("org.jetbrains.kotlinx:kotlinx-html:$htmlVersion") } } jvmMain { @@ -27,7 +29,6 @@ kotlin { } jsMain { dependencies { - api("hep.dataforge:dataforge-output-html:$dataforgeVersion") api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion") api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion") } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Renderer.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Renderer.kt new file mode 100644 index 00000000..820ac615 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Renderer.kt @@ -0,0 +1,5 @@ +package hep.dataforge.vision + +public fun interface Renderer { + public fun render(vision: V) +} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt index d88aaffc..8a2918a1 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt @@ -69,7 +69,7 @@ public open class VisionBase : Vision { } override fun removeChangeListener(owner: Any?) { - listeners.removeAll { it.owner == owner } + listeners.removeAll { owner == null || it.owner == owner } } /** diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroup.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroup.kt index 004693a2..e2101b0c 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroup.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroup.kt @@ -87,12 +87,12 @@ public interface MutableVisionGroup : VisionGroup, VisionContainerBuilder Unit) + public fun onStructureChange(owner: Any?, action: (token: NameToken, before: Vision?, after: Vision?) -> Unit) /** * Remove children change listener */ - public fun removeChildrenChangeListener(owner: Any?) + public fun removeStructureChangeListener(owner: Any?) // public operator fun set(name: Name, child: Vision?) } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt index fa3567b6..767f0e5a 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt @@ -45,7 +45,10 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { } } - private data class StructureChangeListener(val owner: Any?, val callback: (NameToken, Vision?) -> Unit) + private data class StructureChangeListener( + val owner: Any?, + val callback: (token: NameToken, before: Vision?, after: Vision?) -> Unit, + ) @Transient private val structureChangeListeners = HashSet() @@ -53,7 +56,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { /** * Add listener for children change */ - override fun onChildrenChange(owner: Any?, action: (NameToken, Vision?) -> Unit) { + override fun onStructureChange(owner: Any?, action: (token: NameToken, before: Vision?, after: Vision?) -> Unit) { structureChangeListeners.add( StructureChangeListener( owner, @@ -65,29 +68,31 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { /** * Remove children change listener */ - override fun removeChildrenChangeListener(owner: Any?) { - structureChangeListeners.removeAll { it.owner === owner } + override fun removeStructureChangeListener(owner: Any?) { + structureChangeListeners.removeAll { owner == null || it.owner === owner } } /** * Propagate children change event upwards */ - protected fun childrenChanged(name: NameToken, child: Vision?) { - structureChangeListeners.forEach { it.callback(name, child) } + 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) { - childrenInternal.remove(token) + public fun removeChild(token: NameToken): Vision? { + val removed = childrenInternal.remove(token) + removed?.parent = null + return removed } /** * Add a static child. Statics could not be found by name, removed or replaced. Changing statics also do not trigger events. */ protected open fun addStatic(child: Vision): Unit { - attach(NameToken("@static", index = child.hashCode().toString()), child) + attachChild(NameToken("@static", index = child.hashCode().toString()), child) } protected open fun createGroup(): VisionGroupBase = VisionGroupBase() @@ -95,7 +100,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { /** * Set parent for given child and attach it */ - private fun attach(token: NameToken, child: Vision) { + private fun attachChild(token: NameToken, child: Vision) { if (child.parent == null) { child.parent = this childrenInternal[token] = child @@ -114,7 +119,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { val token = name.tokens.first() when (val current = children[token]) { null -> createGroup().also { child -> - attach(token, child) + attachChild(token, child) } is VisionGroupBase -> current else -> error("Can't create group with name $name because it exists and not a group") @@ -137,12 +142,13 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { } name.length == 1 -> { val token = name.tokens.first() + val before = children[token] if (child == null) { removeChild(token) } else { - attach(token, child) + attachChild(token, child) } - childrenChanged(token, child) + childrenChanged(token, before, child) } else -> { //TODO add safety check @@ -165,7 +171,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { when { child is NullVision -> removeChild(token) children.containsKey(token) -> children[token]!!.update(child) - else -> attach(token, child) + else -> attachChild(token, child) } } } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt index 63dc4930..426644ca 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt @@ -48,31 +48,6 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) { encodeToJsonElement(vision).toMetaItem(descriptor).node ?: error("Expected node, but value found. Check your serializer!") -// public fun updateVision(vision: Vision, meta: Meta) { -// vision.update(meta) -// if (vision is MutableVisionGroup) { -// val children by meta.node() -// children?.items?.forEach { (token, item) -> -// when { -// item.value == Null -> vision[token] = null //Null means removal -// item.node != null -> { -// val node = item.node!! -// val type by node.string() -// if (type != null) { -// //If the type is present considering it as new node, not an update -// vision[token.asName()] = decodeFromMeta(node) -// } else { -// val existing = vision.children[token] -// if (existing != null) { -// updateVision(existing, node) -// } -// } -// } -// } -// } -// } -// } - public companion object : PluginFactory { override val tag: PluginTag = PluginTag(name = "vision", group = PluginTag.DATAFORGE_GROUP) override val type: KClass = VisionManager::class diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/collectChange.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/collectChange.kt deleted file mode 100644 index cb61939f..00000000 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/collectChange.kt +++ /dev/null @@ -1,106 +0,0 @@ -package hep.dataforge.vision - -import hep.dataforge.meta.* -import hep.dataforge.meta.descriptors.NodeDescriptor -import hep.dataforge.names.Name -import hep.dataforge.names.NameToken -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlin.time.Duration - -/** - * An empty vision existing only for Vision tree change representation. [NullVision] should not be used outside update logic. - */ -@Serializable -@SerialName("vision.null") -public object NullVision : Vision { - - @Suppress("SetterBackingFieldAssignment") - override var parent: VisionGroup? = null - set(value) { - //do nothing - } - - override val properties: Config? = null - - override fun getAllProperties(): Laminate = Laminate(Meta.EMPTY) - - override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = null - - override fun propertyChanged(name: Name) {} - - override fun onPropertyChange(owner: Any?, action: (Name) -> Unit) {} - - override fun removeChangeListener(owner: Any?) {} - - override fun update(change: Vision) { - error("Null vision should be removed, not updated") - } - - override val config: Config get() = Config() - override val descriptor: NodeDescriptor? get() = null -} - -private fun Vision.collectChange(scope: CoroutineScope, collector: Vision): Job = scope.launch { - //Store job to be able to cancel collection jobs - //TODO add lock for concurrent modification protection? - val jobStore = HashMap() - - if (this is VisionGroup) { - check(collector is MutableVisionGroup) { "Collector for a group should be a group" } - //Subscribe for children changes - children.forEach { (token, child) -> - val childCollector: Vision = if (child is VisionGroup) { - VisionGroupBase() - } else { - VisionBase() - } - val job = child.collectChange(this, childCollector) - jobStore[token] = job - //TODO add lazy child addition - collector[token] = childCollector - } - - //Subscribe for structure change - if (this is MutableVisionGroup) { - onChildrenChange(this) { token, child -> - //Cancel collector job to avoid leaking - jobStore[token]?.cancel() - if (child != null) { - //Collect to existing Vision - val job = child.collectChange(this, child) - jobStore[token] = job - collector[token] = child - } else{ - collector[token] = NullVision - } - } - } - } - - //Collect properties change - collector.onPropertyChange(collector) { propertyName -> - collector.config[propertyName] = properties?.get(propertyName) - } -} - -public fun Vision.flowChanges(scope: CoroutineScope, collectionDuration: Duration): Flow = flow { - //emit initial visual tree - emit(this@flowChanges) - while (true) { - val collector: Vision = if (this is VisionGroup) { - VisionGroupBase() - } else { - VisionBase() - } - val collectorJob = collectChange(scope, collector) - kotlinx.coroutines.delay(collectionDuration) - emit(collector) - collectorJob.cancel() - } -} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionChange.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionChange.kt new file mode 100644 index 00000000..21434e80 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionChange.kt @@ -0,0 +1,141 @@ +package hep.dataforge.vision + +import hep.dataforge.meta.* +import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.names.Name +import hep.dataforge.names.isEmpty +import hep.dataforge.names.plus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.time.Duration + + +public abstract class EmptyVision : Vision { + + @Suppress("SetterBackingFieldAssignment", "UNUSED_PARAMETER") + override var parent: VisionGroup? = null + set(value) { + //do nothing + } + + override val properties: Config? = null + + override fun getAllProperties(): Laminate = Laminate(Meta.EMPTY) + + override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = null + + override fun propertyChanged(name: Name) {} + + override fun onPropertyChange(owner: Any?, action: (Name) -> Unit) {} + + override fun removeChangeListener(owner: Any?) {} + + override fun update(change: Vision) { + error("Null vision should be removed, not updated") + } + + override val config: Config get() = Config() + override val descriptor: NodeDescriptor? get() = null +} + +/** + * An empty vision existing only for Vision tree change representation. [NullVision] should not be used outside update logic. + */ +@Serializable +@SerialName("vision.null") +public object NullVision : EmptyVision() + +@Serializable(VisionSerializationProxy.Companion::class) +private class VisionSerializationProxy(val ref: Vision) : EmptyVision() { + companion object : KSerializer { + override val descriptor: SerialDescriptor = Vision.serializer().descriptor + + @OptIn(ExperimentalSerializationApi::class) + override fun serialize(encoder: Encoder, value: VisionSerializationProxy) { + val serializer = encoder.serializersModule.getPolymorphic(Vision::class, value.ref) + ?: error("The polymorphic serializer is not provided for ") + serializer.serialize(encoder, value.ref) + } + + override fun deserialize(decoder: Decoder): VisionSerializationProxy = + VisionSerializationProxy(Vision.serializer().deserialize(decoder)) + } +} + + +private fun MutableVisionGroup.getOrCreate(name: Name): Vision { + if (name.isEmpty()) return this + val existing = get(name) + return existing ?: VisionGroupBase().also { set(name, it) } +} + +private fun CoroutineScope.collectChange( + name: Name, + source: Vision, + mutex: Mutex, + target: () -> MutableVisionGroup, +) { + val targetVision: () -> Vision = { target().getOrCreate(name) } + //Collect properties change + source.onPropertyChange(mutex) { propertyName -> + launch { + mutex.withLock { + targetVision().setProperty(propertyName, source.getProperty(propertyName)) + } + } + } + + if (source is VisionGroup) { + check(targetVision is MutableVisionGroup) { "Collector for a group should be a group" } + //Subscribe for children changes + source.children.forEach { (token, child) -> + collectChange(name + token, child, mutex, target) + } + //TODO update styles? + + //Subscribe for structure change + if (source is MutableVisionGroup) { + source.onStructureChange(mutex) { token, before, after -> + before?.removeChangeListener(mutex) + (before as? MutableVisionGroup)?.removeStructureChangeListener(mutex) + if (after != null) { + targetVision[token] = VisionSerializationProxy(after) + collectChange(name + token, after, mutex, target) + } else { + targetVision[token] = NullVision + } + } + } + } +} + +@DFExperimental +public fun Vision.flowChanges(scope: CoroutineScope, collectionDuration: Duration): Flow = flow { + //emit initial visual tree + emit(this@flowChanges) + + val mutex = Mutex() + + var collector = VisionGroupBase() + scope.collectChange(Name.EMPTY, this@flowChanges, mutex) { collector } + + while (true) { + //Wait for changes to accumulate + kotlinx.coroutines.delay(collectionDuration) + //Propagate updates only if something is changed + if (collector.children.isNotEmpty() || collector.properties?.isEmpty() != false) { + //emit changes + emit(collector) + //Reset the collector + collector = VisionGroupBase() + } + } +} \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/rendering/HTMLVisionDisplay.kt b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/rendering/HTMLVisionDisplay.kt new file mode 100644 index 00000000..58f2b2b1 --- /dev/null +++ b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/rendering/HTMLVisionDisplay.kt @@ -0,0 +1,20 @@ +package hep.dataforge.vision.rendering + +import hep.dataforge.vision.Renderer +import hep.dataforge.vision.Vision +import org.w3c.dom.HTMLElement + +/** + * A display container factory for specific vision + * @param V type of [Vision] to be rendered + * @param C the specific type of the container + */ +public fun interface HTMLVisionDisplay> { + public fun attachRenderer(element: HTMLElement): C +} + +/** + * Render a specific element and return container for configuration + */ +public fun > HTMLVisionDisplay.render(element: HTMLElement, vision: V): C = + attachRenderer(element).apply { render(vision)} diff --git a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/editor/ValueChooser.kt b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/editor/ValueChooser.kt index df270f35..e1049eef 100644 --- a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/editor/ValueChooser.kt +++ b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/editor/ValueChooser.kt @@ -27,7 +27,7 @@ import tornadofx.* * * @author [Alexander Nozik](mailto:altavir@gmail.com) */ -interface ValueChooser { +public interface ValueChooser { /** * Get or create a Node that could be later inserted into some parent diff --git a/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GDMLTransformer.kt b/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GDMLTransformer.kt index 0c66ac59..75c93cc4 100644 --- a/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GDMLTransformer.kt +++ b/visionforge-gdml/src/commonMain/kotlin/hep/dataforge/vision/gdml/GDMLTransformer.kt @@ -54,9 +54,9 @@ private class GDMLTransformer(val settings: GDMLTransformerSettings) { } - private val referenceStore = HashMap>() + private val referenceStore = HashMap>() - private fun proxySolid(root: GDML, group: SolidGroup, solid: GDMLSolid, name: String): Proxy { + private fun proxySolid(root: GDML, group: SolidGroup, solid: GDMLSolid, name: String): SolidReference { val templateName = solidsName + name if (proto[templateName] == null) { solids.addSolid(root, solid, name) @@ -66,7 +66,7 @@ private class GDMLTransformer(val settings: GDMLTransformerSettings) { return ref } - private fun proxyVolume(root: GDML, group: SolidGroup, physVolume: GDMLPhysVolume, volume: GDMLGroup): Proxy { + private fun proxyVolume(root: GDML, group: SolidGroup, physVolume: GDMLPhysVolume, volume: GDMLGroup): SolidReference { val templateName = volumesName + volume.name.asName() if (proto[templateName] == null) { proto[templateName] = volume(root, volume) diff --git a/visionforge-gdml/src/jvmTest/kotlin/hep/dataforge/vision/gdml/bmanStatistics.kt b/visionforge-gdml/src/jvmTest/kotlin/hep/dataforge/vision/gdml/bmanStatistics.kt index 87b93093..c3e57a66 100644 --- a/visionforge-gdml/src/jvmTest/kotlin/hep/dataforge/vision/gdml/bmanStatistics.kt +++ b/visionforge-gdml/src/jvmTest/kotlin/hep/dataforge/vision/gdml/bmanStatistics.kt @@ -1,14 +1,12 @@ package hep.dataforge.vision.gdml -import hep.dataforge.vision.solid.AbstractProxy import hep.dataforge.vision.solid.prototype import hep.dataforge.vision.visitor.countDistinct -import hep.dataforge.vision.visitor.countDistinctBy import hep.dataforge.vision.visitor.flowStatistics import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import nl.adaptivity.xmlutil.StAXReader import kscience.gdml.GDML +import nl.adaptivity.xmlutil.StAXReader import java.io.File import kotlin.reflect.KClass diff --git a/visionforge-server/build.gradle.kts b/visionforge-server/build.gradle.kts index 17696344..ff726eb5 100644 --- a/visionforge-server/build.gradle.kts +++ b/visionforge-server/build.gradle.kts @@ -6,3 +6,10 @@ plugins { val ktorVersion: String by rootProject.extra +dependencies { + api(project(":visionforge-core")) + api("io.ktor:ktor-server-cio:$ktorVersion") + //api("io.ktor:ktor-server-netty:$ktorVersion") + api("io.ktor:ktor-html-builder:$ktorVersion") + api("io.ktor:ktor-websockets:$ktorVersion") +} \ No newline at end of file diff --git a/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt new file mode 100644 index 00000000..98894710 --- /dev/null +++ b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt @@ -0,0 +1,219 @@ +package hep.dataforge.vision.server + +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE +import io.ktor.application.Application +import io.ktor.application.featureOrNull +import io.ktor.application.install +import io.ktor.features.CORS +import io.ktor.http.content.resources +import io.ktor.http.content.static +import io.ktor.routing.Routing +import io.ktor.routing.route +import io.ktor.routing.routing +import io.ktor.server.engine.ApplicationEngine +import io.ktor.websocket.WebSockets +import kotlinx.html.TagConsumer +import java.awt.Desktop +import java.net.URI +import kotlin.text.get + +public enum class ServerUpdateMode { + NONE, + PUSH, + PULL +} + +public class VisionServer internal constructor( + private val routing: Routing, + private val rootRoute: String, +) : Configurable { + override val config: Config = Config() + public var updateMode: ServerUpdateMode by config.enum(ServerUpdateMode.NONE, key = UPDATE_MODE_KEY) + public var updateInterval: Long by config.long(300, key = UPDATE_INTERVAL_KEY) + public var embedData: Boolean by config.boolean(false) + + /** + * a list of headers that should be applied to all pages + */ + private val globalHeaders: ArrayList = ArrayList() + + public fun header(block: TagConsumer<*>.() -> Unit) { + globalHeaders.add(HtmlFragment(block)) + } + + public fun page( + plotlyFragment: PlotlyFragment, + route: String = DEFAULT_PAGE, + title: String = "Plotly server page '$route'", + headers: List = emptyList(), + ) { + routing.createRouteFromPath(rootRoute).apply { + val plots = HashMap() + route(route) { + //Update websocket + webSocket("ws/{id}") { + val plotId: String = call.parameters["id"] ?: error("Plot id not defined") + + application.log.debug("Opened server socket for $plotId") + + val plot = plots[plotId] ?: error("Plot with id='$plotId' not registered") + + try { + plot.collectUpdates(plotId, this, updateInterval).collect { update -> + val json = update.toJson() + outgoing.send(Frame.Text(json.toString())) + } + } catch (ex: Exception) { + application.log.debug("Closed server socket for $plotId") + } + } + //Plots in their json representation + get("data/{id}") { + val id: String = call.parameters["id"] ?: error("Plot id not defined") + + val plot: Plot? = plots[id] + if (plot == null) { + call.respond(HttpStatusCode.NotFound, "Plot with id = $id not found") + } else { + call.respondText( + plot.toJsonString(), + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK + ) + } + } + //filled pages + get { + val origin = call.request.origin + val url = URLBuilder().apply { + protocol = URLProtocol.createOrDefault(origin.scheme) + //workaround for https://github.com/ktorio/ktor/issues/1663 + host = if (origin.host.startsWith("0:")) "[${origin.host}]" else origin.host + port = origin.port + encodedPath = origin.uri + }.build() + call.respondHtml { + val normalizedRoute = if (rootRoute.endsWith("/")) { + rootRoute + } else { + "$rootRoute/" + } + + head { + meta { + charset = "utf-8" + (globalHeaders + headers).forEach { + it.visit(consumer) + } + script { + type = "text/javascript" + src = "${normalizedRoute}js/plotly.min.js" + } + script { + type = "text/javascript" + src = "${normalizedRoute}js/plotlyConnect.js" + } + } + title(title) + } + body { + val container = + ServerPlotlyRenderer(url, updateMode, updateInterval, embedData) { plotId, plot -> + plots[plotId] = plot + } + with(plotlyFragment) { + render(container) + } + } + } + } + } + } + } + + public fun page( + route: String = DEFAULT_PAGE, + title: String = "Plotly server page '$route'", + headers: List = emptyList(), + content: FlowContent.(renderer: PlotlyRenderer) -> Unit, + ) { + page(PlotlyFragment(content), route, title, headers) + } + + + public companion object { + public const val DEFAULT_PAGE: String = "/" + public val UPDATE_MODE_KEY: Name = "update.mode".toName() + public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName() + } +} + + +/** + * Attach plotly application to given server + */ +public fun Application.visionModule(route: String = DEFAULT_PAGE): VisionServer { + if (featureOrNull(WebSockets) == null) { + install(WebSockets) + } + + if (featureOrNull(CORS) == null) { + install(CORS) { + anyHost() + } + } + + + val routing = routing { + route(route) { + static { + resources() + } + } + } + + return VisionServer(routing, route) +} + + +/** + * Configure server to start sending updates in push mode. Does not affect loaded pages + */ +public fun VisionServer.pushUpdates(interval: Long = 100): VisionServer = apply { + updateMode = ServerUpdateMode.PUSH + updateInterval = interval +} + +/** + * Configure client to request regular updates from server. Pull updates are more expensive than push updates since + * they contain the full plot data and server can't decide what to send. + */ +public fun VisionServer.pullUpdates(interval: Long = 1000): VisionServer = apply { + updateMode = ServerUpdateMode.PULL + updateInterval = interval +} + +///** +// * Start static server (updates via reload) +// */ +//@OptIn(KtorExperimentalAPI::class) +//public fun Plotly.serve( +// scope: CoroutineScope = GlobalScope, +// host: String = "localhost", +// port: Int = 7777, +// block: PlotlyServer.() -> Unit, +//): ApplicationEngine = scope.embeddedServer(io.ktor.server.cio.CIO, port, host) { +// plotlyModule().apply(block) +//}.start() + + +public fun ApplicationEngine.show() { + val connector = environment.connectors.first() + val uri = URI("http", null, connector.host, connector.port, null, null, null) + Desktop.getDesktop().browse(uri) +} + +public fun ApplicationEngine.close(): Unit = stop(1000, 5000) \ No newline at end of file diff --git a/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/html.kt b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/html.kt new file mode 100644 index 00000000..d6df7edc --- /dev/null +++ b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/html.kt @@ -0,0 +1,70 @@ +package hep.dataforge.vision.server + +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption + +public class HtmlFragment(public val visit: TagConsumer<*>.() -> Unit) { + override fun toString(): String { + return createHTML().also(visit).finalize() + } +} + +public operator fun HtmlFragment.plus(other: HtmlFragment): HtmlFragment = HtmlFragment { + this@plus.run { visit() } + other.run { visit() } +} + +/** + * Check if the asset exists in given local location and put it there if it does not + */ +internal fun checkOrStoreFile(basePath: Path, filePath: Path, resource: String): Path { + val fullPath = basePath.resolveSibling(filePath).toAbsolutePath() + + if (Files.exists(fullPath)) { + //TODO checksum + } else { + //TODO add logging + + val bytes = HtmlFragment::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) + } else { + filePath + } +} + +/** + * A header that automatically copies relevant scripts to given path + */ +public fun localScriptHeader( + basePath: Path, + scriptPath: Path, + resource: String +): HtmlFragment = HtmlFragment { + val relativePath = checkOrStoreFile(basePath, scriptPath, resource) + script { + type = "text/javascript" + src = relativePath.toString() + attributes["onload"] = "console.log('Script successfully loaded from $relativePath')" + attributes["onerror"] = "console.log('Failed to load script from $relativePath')" + } +} + +public fun localCssHeader( + basePath: Path, + cssPath: Path, + resource: String +): HtmlFragment = HtmlFragment { + val relativePath = checkOrStoreFile(basePath, cssPath, resource) + link { + rel = "stylesheet" + href = relativePath.toString() + } +} \ No newline at end of file diff --git a/visionforge-solid/build.gradle.kts b/visionforge-solid/build.gradle.kts index 3f7ac5dc..a9569b76 100644 --- a/visionforge-solid/build.gradle.kts +++ b/visionforge-solid/build.gradle.kts @@ -29,7 +29,7 @@ kotlin { } jsMain { dependencies { - implementation(npm("three", "0.114.0")) + implementation(npm("three", "0.122.0")) implementation(npm("three-csg-ts", "1.0.1")) } } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt index 6e176501..024f5215 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt @@ -5,9 +5,9 @@ import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.names.plus -import hep.dataforge.output.Renderer import hep.dataforge.values.ValueType import hep.dataforge.values.asValue +import hep.dataforge.vision.Renderer import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision.Companion.VISIBLE_KEY import hep.dataforge.vision.enum @@ -73,11 +73,11 @@ public interface Solid : Vision { item(SolidMaterial.MATERIAL_KEY.toString(), SolidMaterial.descriptor) - enum(ROTATION_ORDER_KEY,default = RotationOrder.XYZ) + enum(ROTATION_ORDER_KEY, default = RotationOrder.XYZ) } } - internal fun solidEquals(first: Solid, second: Solid): Boolean{ + internal fun solidEquals(first: Solid, second: Solid): Boolean { if (first.position != second.position) return false if (first.rotation != second.rotation) return false if (first.scale != second.scale) return false @@ -85,8 +85,8 @@ public interface Solid : Vision { return true } - internal fun solidHashCode(solid: Solid): Int{ - var result = + (solid.position?.hashCode() ?: 0) + internal fun solidHashCode(solid: Solid): Int { + 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) @@ -104,8 +104,7 @@ public var Solid.layer: Int config[LAYER_KEY] = value.asValue() } -public fun Renderer.render(meta: Meta = Meta.EMPTY, action: SolidGroup.() -> Unit): Unit = - render(SolidGroup().apply(action), meta) +public fun Renderer.render(action: SolidGroup.() -> Unit): Unit = render(SolidGroup().apply(action)) // Common properties diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidManager.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidManager.kt index be737599..319fffca 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidManager.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidManager.kt @@ -35,7 +35,7 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) { private fun PolymorphicModuleBuilder.solids() { subclass(SolidGroup.serializer()) - subclass(Proxy.serializer()) + subclass(SolidReference.serializer()) subclass(Composite.serializer()) subclass(Tube.serializer()) subclass(Box.serializer()) diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Proxy.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt similarity index 74% rename from visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Proxy.kt rename to visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt index 55946255..266366ce 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Proxy.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlin.collections.set -public abstract class AbstractProxy : BasicSolid(), VisionGroup { +public abstract class AbstractReference : BasicSolid(), VisionGroup { public abstract val prototype: Solid override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { @@ -46,13 +46,13 @@ public abstract class AbstractProxy : BasicSolid(), VisionGroup { } /** - * A proxy [Solid] to reuse a template object + * A reference [Solid] to reuse a template object */ @Serializable -@SerialName("solid.proxy") -public class Proxy( +@SerialName("solid.ref") +public class SolidReference( public val templateName: Name, -) : AbstractProxy(), Solid { +) : AbstractReference(), Solid { /** * Recursively search for defined template in the parent @@ -67,15 +67,15 @@ public class Proxy( private val propertyCache: HashMap = HashMap() - override val children: Map + override val children: Map get() = (prototype as? VisionGroup)?.children ?.filter { !it.key.toString().startsWith("@") } ?.mapValues { - ProxyChild(it.key.asName()) + ReferenceChild(it.key.asName()) } ?: emptyMap() private fun childPropertyName(childName: Name, propertyName: Name): Name { - return NameToken(PROXY_CHILD_PROPERTY_PREFIX, childName.toString()) + propertyName + return NameToken(REFERENCE_CHILD_PROPERTY_PREFIX, childName.toString()) + propertyName } private fun prototypeFor(name: Name): Solid { @@ -89,17 +89,17 @@ public class Proxy( * 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 ProxyChild(public val name: Name) : AbstractProxy() { + public inner class ReferenceChild(public val name: Name) : AbstractReference() { override val prototype: Solid get() = prototypeFor(name) - override val styleSheet: StyleSheet get() = this@Proxy.styleSheet + override val styleSheet: StyleSheet get() = this@SolidReference.styleSheet override val children: Map get() = (prototype as? VisionGroup)?.children ?.filter { !it.key.toString().startsWith("@") } ?.mapValues { (key, _) -> - ProxyChild(name + key.asName()) + ReferenceChild(name + key.asName()) } ?: emptyMap() override var properties: Config? @@ -108,12 +108,12 @@ public class Proxy( if (value == null) { propertyCache.remove(name)?.also { //Removing listener if it is present - removeChangeListener(this@Proxy) + removeChangeListener(this@SolidReference) } } else { propertyCache[name] = value.also { - onPropertyChange(this@Proxy) { propertyName -> - this@Proxy.propertyChanged(childPropertyName(name, propertyName)) + onPropertyChange(this@SolidReference) { propertyName -> + this@SolidReference.propertyChanged(childPropertyName(name, propertyName)) } } } @@ -122,16 +122,16 @@ public class Proxy( } public companion object { - public const val PROXY_CHILD_PROPERTY_PREFIX: String = "@child" + public const val REFERENCE_CHILD_PROPERTY_PREFIX: String = "@child" } } /** - * Get a vision prototype if it is a [Proxy] or vision itself if it is not + * Get a vision prototype if it is a [SolidReference] or vision itself if it is not */ public val Vision.prototype: Vision get() = when (this) { - is AbstractProxy -> prototype + is AbstractReference -> prototype else -> this } @@ -141,16 +141,16 @@ public val Vision.prototype: Vision public fun SolidGroup.ref( templateName: Name, name: String = "", -): Proxy = Proxy(templateName).also { set(name, it) } +): SolidReference = SolidReference(templateName).also { set(name, it) } /** - * Add new proxy wrapping given object and automatically adding it to the prototypes + * Add new [SolidReference] wrapping given object and automatically adding it to the prototypes */ -public fun SolidGroup.proxy( +public fun SolidGroup.ref( name: String, obj: Solid, templateName: Name = name.toName(), -): Proxy { +): SolidReference { val existing = getPrototype(templateName) if (existing == null) { prototypes { @@ -159,5 +159,5 @@ public fun SolidGroup.proxy( } else if (existing != obj) { error("Can't add different prototype on top of existing one") } - return ref(templateName, name) + return this@ref.ref(templateName, name) } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt index d39daaf0..b62ea275 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt @@ -1,6 +1,7 @@ package hep.dataforge.vision.solid.specifications import hep.dataforge.meta.* +import hep.dataforge.names.Name public class Canvas3DOptions : Scheme() { public var axes: Axes by spec(Axes, Axes.empty()) @@ -15,6 +16,8 @@ public class Canvas3DOptions : Scheme() { public var maxWith: Number by number { maxSize } public var maxHeight: Number by number { maxSize } + public var onSelect: ((Name?)->Unit)? = null + public companion object : SchemeSpec(::Canvas3DOptions) } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/RemoveSingleChild.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/RemoveSingleChild.kt index 3f205893..7bedebe4 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/RemoveSingleChild.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/RemoveSingleChild.kt @@ -40,7 +40,7 @@ internal object RemoveSingleChild : VisualTreeTransform() { override fun SolidGroup.transformInPlace() { fun MutableVisionGroup.replaceChildren() { children.forEach { (childName, parent) -> - if (parent is Proxy) return@forEach //ignore refs + if (parent is SolidReference) return@forEach //ignore refs if (parent is MutableVisionGroup) { parent.replaceChildren() } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/UnRef.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/UnRef.kt index c3f2109b..d45c2ca2 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/UnRef.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/transform/UnRef.kt @@ -5,8 +5,8 @@ import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.vision.MutableVisionGroup import hep.dataforge.vision.VisionGroup -import hep.dataforge.vision.solid.Proxy import hep.dataforge.vision.solid.SolidGroup +import hep.dataforge.vision.solid.SolidReference @DFExperimental internal object UnRef : VisualTreeTransform() { @@ -17,7 +17,7 @@ internal object UnRef : VisualTreeTransform() { counter.forEach { (key, value) -> reducer[key] = (reducer[key] ?: 0) + value } - } else if (obj is Proxy) { + } else if (obj is SolidReference) { reducer[obj.templateName] = (reducer[obj.templateName] ?: 0) + 1 } @@ -27,9 +27,9 @@ internal object UnRef : VisualTreeTransform() { private fun MutableVisionGroup.unref(name: Name) { (this as? SolidGroup)?.prototypes?.set(name, null) - children.filter { (it.value as? Proxy)?.templateName == name }.forEach { (key, value) -> - val proxy = value as Proxy - val newChild = mergeChild(proxy, proxy.prototype) + children.filter { (it.value as? SolidReference)?.templateName == name }.forEach { (key, value) -> + val reference = value as SolidReference + val newChild = mergeChild(reference, reference.prototype) newChild.parent = null set(key.asName(), newChild) // replace proxy with merged object } diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/PropertyTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/PropertyTest.kt index 02a0b661..7c80a6fb 100644 --- a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/PropertyTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/PropertyTest.kt @@ -58,8 +58,8 @@ class PropertyTest { } @Test - fun testProxyStyleProperty() { - var box: Proxy? = null + fun testReferenceStyleProperty() { + var box: SolidReference? = null val group = SolidGroup{ styleSheet { set("testStyle") { diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SerializationTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SerializationTest.kt index c9abfce1..efef7fcd 100644 --- a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SerializationTest.kt +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/SerializationTest.kt @@ -3,7 +3,6 @@ package hep.dataforge.vision.solid import hep.dataforge.names.Name import hep.dataforge.names.toName import hep.dataforge.vision.MutableVisionGroup -import hep.dataforge.vision.Vision import hep.dataforge.vision.get import kotlin.test.Test import kotlin.test.assertEquals @@ -12,13 +11,13 @@ import kotlin.test.assertEquals /** * Create and attach new proxied group */ -fun SolidGroup.proxyGroup( +fun SolidGroup.refGroup( name: String, templateName: Name = name.toName(), block: MutableVisionGroup.() -> Unit -): Proxy { +): SolidReference { val group = SolidGroup().apply(block) - return proxy(name, group, templateName) + return ref(name, group, templateName) } @@ -44,8 +43,8 @@ class SerializationTest { z = -100 } val group = SolidGroup{ - proxy("cube", cube) - proxyGroup("pg", "pg.content".toName()){ + ref("cube", cube) + refGroup("pg", "pg.content".toName()){ sphere(50){ x = -100 } diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/DatCanvasControls.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/DatCanvasControls.kt deleted file mode 100644 index 361e18e3..00000000 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/DatCanvasControls.kt +++ /dev/null @@ -1,5 +0,0 @@ -package hep.dataforge.vision.solid.three - -fun createDataControls() { - val dat = kotlinext.js.require("dat.gui") -} \ No newline at end of file diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeCanvas.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeCanvas.kt index 14a5e12e..834fcf70 100644 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeCanvas.kt +++ b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeCanvas.kt @@ -1,12 +1,12 @@ package hep.dataforge.vision.solid.three -import hep.dataforge.context.Context -import hep.dataforge.meta.* +import hep.dataforge.meta.getItem +import hep.dataforge.meta.string import hep.dataforge.names.Name import hep.dataforge.names.plus import hep.dataforge.names.toName -import hep.dataforge.output.Renderer import hep.dataforge.vision.Colors +import hep.dataforge.vision.Renderer import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.specifications.* import hep.dataforge.vision.solid.three.ThreeMaterials.HIGHLIGHT_MATERIAL @@ -41,28 +41,24 @@ import kotlin.math.sin public class ThreeCanvas( public val three: ThreePlugin, public val options: Canvas3DOptions, - public val onClick: ((Name?) -> Unit)? = null, ) : Renderer { - - override val context: Context get() = three.context - - public var content: Solid? = null - private set - private var root: Object3D? = null private val raycaster = Raycaster() private val mousePosition: Vector2 = Vector2() - public val axes: AxesHelper = AxesHelper(options.axes.size.toInt()).apply { - visible = options.axes.visible - } + public var content: Solid? = null + private set - public val scene: Scene = Scene().apply { + public var axes: AxesHelper = AxesHelper(options.axes.size.toInt()).apply { visible = options.axes.visible } + private set + + private val scene: Scene = Scene().apply { add(axes) } - public val camera: PerspectiveCamera = buildCamera(options.camera) + public var camera: PerspectiveCamera = buildCamera(options.camera) + private set private var picked: Object3D? = null @@ -97,7 +93,7 @@ public class ThreeCanvas( element.addEventListener("mousedown", { val picked = pick() - onClick?.invoke(picked?.fullName()) + options.onSelect?.invoke(picked?.fullName()) }, false) val renderer = WebGLRenderer { antialias = true }.apply { @@ -193,15 +189,14 @@ public class ThreeCanvas( } } - override fun render(obj: Solid, meta: Meta) { + public override fun render(vision: Solid) { //clear old root clear() - - val object3D = three.buildObject3D(obj) + val object3D = three.buildObject3D(vision) object3D.name = "@root" scene.add(object3D) - content = obj + content = vision root = object3D } @@ -260,17 +255,4 @@ public class ThreeCanvas( private const val HIGHLIGHT_NAME = "@highlight" private const val SELECT_NAME = "@select" } -} - -public fun ThreePlugin.output( - element: HTMLElement, - spec: Canvas3DOptions = Canvas3DOptions.empty(), - onClick: ((Name?) -> Unit)? = null, -): ThreeCanvas = ThreeCanvas(this, spec, onClick).apply { attach(element) } - -public fun ThreePlugin.render( - element: HTMLElement, - obj: Solid, - onSelect: ((Name?) -> Unit)? = null, - options: Canvas3DOptions.() -> Unit = {}, -): Unit = output(element, Canvas3DOptions.invoke(options), onSelect).render(obj) \ No newline at end of file +} \ No newline at end of file diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeGeometryBuilder.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeGeometryBuilder.kt index 152af45a..c06a0ff6 100644 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeGeometryBuilder.kt +++ b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeGeometryBuilder.kt @@ -13,7 +13,7 @@ import info.laht.threekt.math.Vector3 /** * An implementation of geometry builder for Three.js [BufferGeometry] */ -class ThreeGeometryBuilder : GeometryBuilder { +public class ThreeGeometryBuilder : GeometryBuilder { private val vertices = ArrayList() private val faces = ArrayList() diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreePlugin.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreePlugin.kt index b3a9200e..6cbdcc10 100644 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreePlugin.kt +++ b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreePlugin.kt @@ -2,21 +2,28 @@ package hep.dataforge.vision.solid.three import hep.dataforge.context.* import hep.dataforge.meta.Meta +import hep.dataforge.meta.empty +import hep.dataforge.meta.invoke import hep.dataforge.names.* import hep.dataforge.vision.Vision +import hep.dataforge.vision.rendering.HTMLVisionDisplay import hep.dataforge.vision.solid.* +import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.visible import info.laht.threekt.core.Object3D +import org.w3c.dom.HTMLElement import kotlin.collections.set import kotlin.reflect.KClass import info.laht.threekt.objects.Group as ThreeGroup -public class ThreePlugin : AbstractPlugin() { +public class ThreePlugin : AbstractPlugin(), HTMLVisionDisplay { override val tag: PluginTag get() = Companion.tag + public val solidManager: SolidManager by require(SolidManager) + private val objectFactories = HashMap, ThreeFactory<*>>() private val compositeFactory = ThreeCompositeFactory(this) - private val proxyFactory = ThreeProxyFactory(this) + private val refFactory = ThreeReferenceFactory(this) init { //Add specialized factories here @@ -38,7 +45,7 @@ public class ThreePlugin : AbstractPlugin() { public fun buildObject3D(obj: Solid): Object3D { return when (obj) { is ThreeVision -> obj.render() - is Proxy -> proxyFactory(obj) + is SolidReference -> refFactory(obj) is SolidGroup -> { val group = ThreeGroup() obj.children.forEach { (token, child) -> @@ -56,7 +63,7 @@ public class ThreePlugin : AbstractPlugin() { updatePosition(obj) //obj.onChildrenChange() - obj.onPropertyChange(this) { name-> + obj.onPropertyChange(this) { name -> if ( name.startsWith(Solid.POSITION_KEY) || name.startsWith(Solid.ROTATION) || @@ -69,7 +76,7 @@ public class ThreePlugin : AbstractPlugin() { } } - obj.onChildrenChange(this) { nameToken, child -> + obj.onStructureChange(this) { nameToken, _, child -> // if (name.isEmpty()) { // logger.error { "Children change with empty name on $group" } // return@onChildrenChange @@ -108,6 +115,12 @@ public class ThreePlugin : AbstractPlugin() { } } + override fun attachRenderer(element: HTMLElement): ThreeCanvas { + return ThreeCanvas(this, Canvas3DOptions.empty()).apply { + attach(element) + } + } + public companion object : PluginFactory { override val tag: PluginTag = PluginTag("visual.three", PluginTag.DATAFORGE_GROUP) override val type: KClass = ThreePlugin::class @@ -115,6 +128,17 @@ public class ThreePlugin : AbstractPlugin() { } } +public fun ThreePlugin.attachRenderer( + element: HTMLElement, + options: Canvas3DOptions = Canvas3DOptions.empty(), +): ThreeCanvas = ThreeCanvas(this, options).apply { attach(element) } + +public fun ThreePlugin.render( + element: HTMLElement, + obj: Solid, + options: Canvas3DOptions.() -> Unit = {}, +): ThreeCanvas = attachRenderer(element, Canvas3DOptions(options)).apply { render(obj) } + internal operator fun Object3D.set(token: NameToken, object3D: Object3D) { object3D.name = token.toString() add(object3D) diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeProxyFactory.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeReferenceFactory.kt similarity index 69% rename from visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeProxyFactory.kt rename to visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeReferenceFactory.kt index b1b17fe6..32ea564f 100644 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeProxyFactory.kt +++ b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeReferenceFactory.kt @@ -3,18 +3,18 @@ package hep.dataforge.vision.solid.three import hep.dataforge.names.cutFirst import hep.dataforge.names.firstOrNull import hep.dataforge.names.toName -import hep.dataforge.vision.solid.Proxy -import hep.dataforge.vision.solid.Proxy.Companion.PROXY_CHILD_PROPERTY_PREFIX import hep.dataforge.vision.solid.Solid +import hep.dataforge.vision.solid.SolidReference +import hep.dataforge.vision.solid.SolidReference.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 ThreeProxyFactory(public val three: ThreePlugin) : ThreeFactory { +public class ThreeReferenceFactory(public val three: ThreePlugin) : ThreeFactory { private val cache = HashMap() - override val type: KClass = Proxy::class + override val type: KClass = SolidReference::class private fun Object3D.replicate(): Object3D { return when (this) { @@ -30,7 +30,7 @@ public class ThreeProxyFactory(public val three: ThreePlugin) : ThreeFactory - if (name.firstOrNull()?.body == PROXY_CHILD_PROPERTY_PREFIX) { - val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for proxy child property: '$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() - val proxyChild = obj[childName] ?: error("Proxy child with name '$childName' not found") + val referenceChild = obj[childName] ?: error("Reference child with name '$childName' not found") val child = object3D.findChild(childName) ?: error("Object child with name '$childName' not found") - child.updateProperty(proxyChild, propertyName) + child.updateProperty(referenceChild, propertyName) } else { object3D.updateProperty(obj, name) } diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/three.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/three.kt index 2b35b7e7..b18cd2eb 100644 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/three.kt +++ b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/three.kt @@ -14,15 +14,15 @@ import info.laht.threekt.objects.Mesh import info.laht.threekt.textures.Texture import kotlin.math.PI -val Solid.euler get() = Euler(rotationX, rotationY, rotationZ, rotationOrder.name) +public val Solid.euler: Euler get() = Euler(rotationX, rotationY, rotationZ, rotationOrder.name) -val MetaItem<*>.vector 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) -fun Geometry.toBufferGeometry(): BufferGeometry = BufferGeometry().apply { fromGeometry(this@toBufferGeometry) } +public fun Geometry.toBufferGeometry(): BufferGeometry = BufferGeometry().apply { fromGeometry(this@toBufferGeometry) } internal fun Double.toRadians() = this * PI / 180 -fun CSG.toGeometry(): Geometry { +public fun CSG.toGeometry(): Geometry { val geom = Geometry() val vertices = ArrayList() diff --git a/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FX3DPlugin.kt b/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FX3DPlugin.kt index 8d90d3b2..be2f5788 100644 --- a/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FX3DPlugin.kt +++ b/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FX3DPlugin.kt @@ -28,7 +28,7 @@ class FX3DPlugin : AbstractPlugin() { private val objectFactories = HashMap, FX3DFactory<*>>() private val compositeFactory = FXCompositeFactory(this) - private val proxyFactory = FXProxyFactory(this) + private val referenceFactory = FXReferenceFactory(this) init { //Add specialized factories here @@ -45,7 +45,7 @@ class FX3DPlugin : AbstractPlugin() { fun buildNode(obj: Solid): Node { val binding = VisualObjectFXBinding(obj) return when (obj) { - is Proxy -> proxyFactory(obj, binding) + is SolidReference -> referenceFactory(obj, binding) is SolidGroup -> { Group(obj.children.mapNotNull { (token, obj) -> (obj as? Solid)?.let { diff --git a/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXCanvas3D.kt b/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXCanvas3D.kt index 1c90819b..e8989840 100644 --- a/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXCanvas3D.kt +++ b/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXCanvas3D.kt @@ -2,9 +2,8 @@ package hep.dataforge.vision.solid.fx import hep.dataforge.context.Context import hep.dataforge.context.ContextAware -import hep.dataforge.meta.Meta import hep.dataforge.meta.empty -import hep.dataforge.output.Renderer +import hep.dataforge.vision.Renderer import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.specifications.Canvas3DOptions import javafx.application.Platform @@ -81,7 +80,7 @@ class FXCanvas3D(val plugin: FX3DPlugin, val spec: Canvas3DOptions = Canvas3DOpt } } - override fun render(obj: Solid, meta: Meta) { - rootObject = obj + override fun render(vision: Solid) { + rootObject = vision } } \ No newline at end of file diff --git a/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXProxyFactory.kt b/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXReferenceFactory.kt similarity index 63% rename from visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXProxyFactory.kt rename to visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXReferenceFactory.kt index 3dca5113..6f6912e3 100644 --- a/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXProxyFactory.kt +++ b/visionforge-solid/src/jvmMain/kotlin/hep/dataforge/vision/solid/fx/FXReferenceFactory.kt @@ -2,25 +2,25 @@ package hep.dataforge.vision.solid.fx import hep.dataforge.names.* import hep.dataforge.vision.Vision -import hep.dataforge.vision.solid.Proxy +import hep.dataforge.vision.solid.SolidReference import javafx.scene.Group import javafx.scene.Node import kotlin.reflect.KClass -class FXProxyFactory(val plugin: FX3DPlugin) : FX3DFactory { - override val type: KClass get() = Proxy::class +class FXReferenceFactory(val plugin: FX3DPlugin) : FX3DFactory { + override val type: KClass get() = SolidReference::class - override fun invoke(obj: Proxy, binding: VisualObjectFXBinding): Node { + override fun invoke(obj: SolidReference, binding: VisualObjectFXBinding): Node { val prototype = obj.prototype val node = plugin.buildNode(prototype) obj.onPropertyChange(this) { name-> - if (name.firstOrNull()?.body == Proxy.PROXY_CHILD_PROPERTY_PREFIX) { - val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for proxy child property: '$name'") + if (name.firstOrNull()?.body == SolidReference.REFERENCE_CHILD_PROPERTY_PREFIX) { + val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for reference child property: '$name'") val propertyName = name.cutFirst() - val proxyChild = obj[childName] ?: error("Proxy child with name '$childName' not found") + val referenceChild = obj[childName] ?: error("Reference child with name '$childName' not found") val child = node.findChild(childName) ?: error("Object child with name '$childName' not found") - child.updateProperty(proxyChild, propertyName) + child.updateProperty(referenceChild, propertyName) } } return node