Remove dataforge-output.

This commit is contained in:
Alexander Nozik 2020-11-15 15:00:54 +03:00
parent 613624ff17
commit 6a48948c15
38 changed files with 620 additions and 284 deletions

View File

@ -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. for multiple objects. This helps to significantly decrease memory usage.
The `prototypes` property tree is defined in `SolidGroup` class via `PrototypeHolder` interface, and 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 ##### Styles

View File

@ -6,7 +6,10 @@ plugins {
val dataforgeVersion by extra("0.2.0-dev-7") val dataforgeVersion by extra("0.2.0-dev-7")
val ktorVersion by extra("1.4.2") 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 kotlinWrappersVersion by extra("pre.129-kotlin-1.4.10")
val fxVersion by extra("14")
allprojects { allprojects {
repositories { repositories {
@ -21,7 +24,6 @@ allprojects {
val githubProject by extra("visionforge") val githubProject by extra("visionforge")
val bintrayRepo by extra("dataforge") val bintrayRepo by extra("dataforge")
val fxVersion by extra("14")
subprojects { subprojects {
if(name.startsWith("visionforge")) { if(name.startsWith("visionforge")) {

View File

@ -5,12 +5,11 @@ import hep.dataforge.meta.Meta
import hep.dataforge.meta.get import hep.dataforge.meta.get
import hep.dataforge.meta.string import hep.dataforge.meta.string
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.output.OutputManager
import hep.dataforge.output.Renderer import hep.dataforge.output.Renderer
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreeCanvas
import hep.dataforge.vision.solid.three.ThreePlugin 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.browser.document
import kotlinx.dom.clear import kotlinx.dom.clear
import kotlinx.html.dom.append import kotlinx.html.dom.append
@ -23,7 +22,7 @@ import kotlinx.html.span
import org.w3c.dom.Element import org.w3c.dom.Element
import kotlin.reflect.KClass 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 gridRoot = document.create.div("row")
private val outputs: MutableMap<Name, ThreeCanvas> = HashMap() private val outputs: MutableMap<Name, ThreeCanvas> = HashMap()
@ -46,7 +45,7 @@ class ThreeDemoGrid(element: Element, meta: Meta = Meta.EMPTY) : OutputManager {
span("border") { span("border") {
div("col-6") { div("col-6") {
div { id = "output-$name" }.also { div { id = "output-$name" }.also {
output = three.output(it, canvasOptions) output = three.attachRenderer(it, canvasOptions)
//output.attach(it) //output.attach(it)
} }
hr() hr()

View File

@ -3,7 +3,6 @@ package hep.dataforge.vision.bootstrap
import hep.dataforge.vision.react.flexColumn import hep.dataforge.vision.react.flexColumn
import hep.dataforge.vision.react.flexRow import hep.dataforge.vision.react.flexRow
import hep.dataforge.vision.solid.SolidGroup import hep.dataforge.vision.solid.SolidGroup
import hep.dataforge.vision.solid.SolidManager
import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreeCanvas
import kotlinx.css.* import kotlinx.css.*
import kotlinx.css.properties.border import kotlinx.css.properties.border
@ -45,7 +44,7 @@ public external interface CanvasControlsProps : RProps {
public val CanvasControls: FunctionalComponent<CanvasControlsProps> = functionalComponent("CanvasControls") { props -> public val CanvasControls: FunctionalComponent<CanvasControlsProps> = functionalComponent("CanvasControls") { props ->
val visionManager = useMemo( val visionManager = useMemo(
{ props.canvas.context.plugins.fetch(SolidManager).visionManager }, { props.canvas.three.solidManager.visionManager },
arrayOf(props.canvas) arrayOf(props.canvas)
) )
flexColumn { flexColumn {

View File

@ -7,7 +7,6 @@ import hep.dataforge.vision.solid.Solid
import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.solid.specifications.Canvas3DOptions
import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreeCanvas
import hep.dataforge.vision.solid.three.ThreePlugin import hep.dataforge.vision.solid.three.ThreePlugin
import hep.dataforge.vision.solid.three.output
import kotlinx.css.* import kotlinx.css.*
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
@ -40,7 +39,7 @@ public val ThreeCanvasComponent: FunctionalComponent<ThreeCanvasProps> = functio
val element = elementRef.current as? HTMLElement ?: error("Canvas element not found") val element = elementRef.current as? HTMLElement ?: error("Canvas element not found")
val three: ThreePlugin = props.context.plugins.fetch(ThreePlugin) val three: ThreePlugin = props.context.plugins.fetch(ThreePlugin)
val newCanvas: ThreeCanvas = 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) props.canvasCallback?.invoke(newCanvas)
canvas = newCanvas canvas = newCanvas
} }

View File

@ -4,13 +4,15 @@ plugins {
val dataforgeVersion: String by rootProject.extra val dataforgeVersion: String by rootProject.extra
val kotlinWrappersVersion: String by rootProject.extra val kotlinWrappersVersion: String by rootProject.extra
val htmlVersion: String by rootProject.extra
kotlin { kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api("hep.dataforge:dataforge-output:$dataforgeVersion") api("hep.dataforge:dataforge-context:$dataforgeVersion")
api("org.jetbrains.kotlinx:kotlinx-html:$htmlVersion")
} }
} }
jvmMain { jvmMain {
@ -27,7 +29,6 @@ kotlin {
} }
jsMain { jsMain {
dependencies { dependencies {
api("hep.dataforge:dataforge-output-html:$dataforgeVersion")
api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion") api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion")
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion") api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
} }

View File

@ -0,0 +1,5 @@
package hep.dataforge.vision
public fun interface Renderer<in V: Vision> {
public fun render(vision: V)
}

View File

@ -69,7 +69,7 @@ public open class VisionBase : Vision {
} }
override fun removeChangeListener(owner: Any?) { override fun removeChangeListener(owner: Any?) {
listeners.removeAll { it.owner == owner } listeners.removeAll { owner == null || it.owner == owner }
} }
/** /**

View File

@ -87,12 +87,12 @@ public interface MutableVisionGroup : VisionGroup, VisionContainerBuilder<Vision
* @param owner the handler to properly remove listeners * @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. * @param action First argument of the action is the name of changed child. Second argument is the new value of the object.
*/ */
public fun onChildrenChange(owner: Any?, action: (NameToken, Vision?) -> Unit) public fun onStructureChange(owner: Any?, action: (token: NameToken, before: Vision?, after: Vision?) -> Unit)
/** /**
* Remove children change listener * Remove children change listener
*/ */
public fun removeChildrenChangeListener(owner: Any?) public fun removeStructureChangeListener(owner: Any?)
// public operator fun set(name: Name, child: Vision?) // public operator fun set(name: Name, child: Vision?)
} }

View File

@ -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 @Transient
private val structureChangeListeners = HashSet<StructureChangeListener>() private val structureChangeListeners = HashSet<StructureChangeListener>()
@ -53,7 +56,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
/** /**
* Add listener for children change * 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( structureChangeListeners.add(
StructureChangeListener( StructureChangeListener(
owner, owner,
@ -65,29 +68,31 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
/** /**
* Remove children change listener * Remove children change listener
*/ */
override fun removeChildrenChangeListener(owner: Any?) { override fun removeStructureChangeListener(owner: Any?) {
structureChangeListeners.removeAll { it.owner === owner } structureChangeListeners.removeAll { owner == null || it.owner === owner }
} }
/** /**
* Propagate children change event upwards * Propagate children change event upwards
*/ */
protected fun childrenChanged(name: NameToken, child: Vision?) { protected fun childrenChanged(name: NameToken, before: Vision?, after: Vision?) {
structureChangeListeners.forEach { it.callback(name, child) } structureChangeListeners.forEach { it.callback(name, before, after) }
} }
/** /**
* Remove a child with given name token * Remove a child with given name token
*/ */
public fun removeChild(token: NameToken) { public fun removeChild(token: NameToken): Vision? {
childrenInternal.remove(token) 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. * 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 { 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() protected open fun createGroup(): VisionGroupBase = VisionGroupBase()
@ -95,7 +100,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
/** /**
* Set parent for given child and attach it * 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) { if (child.parent == null) {
child.parent = this child.parent = this
childrenInternal[token] = child childrenInternal[token] = child
@ -114,7 +119,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
val token = name.tokens.first() val token = name.tokens.first()
when (val current = children[token]) { when (val current = children[token]) {
null -> createGroup().also { child -> null -> createGroup().also { child ->
attach(token, child) attachChild(token, child)
} }
is VisionGroupBase -> current is VisionGroupBase -> current
else -> error("Can't create group with name $name because it exists and not a group") 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 -> { name.length == 1 -> {
val token = name.tokens.first() val token = name.tokens.first()
val before = children[token]
if (child == null) { if (child == null) {
removeChild(token) removeChild(token)
} else { } else {
attach(token, child) attachChild(token, child)
} }
childrenChanged(token, child) childrenChanged(token, before, child)
} }
else -> { else -> {
//TODO add safety check //TODO add safety check
@ -165,7 +171,7 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
when { when {
child is NullVision -> removeChild(token) child is NullVision -> removeChild(token)
children.containsKey(token) -> children[token]!!.update(child) children.containsKey(token) -> children[token]!!.update(child)
else -> attach(token, child) else -> attachChild(token, child)
} }
} }
} }

View File

@ -48,31 +48,6 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
encodeToJsonElement(vision).toMetaItem(descriptor).node encodeToJsonElement(vision).toMetaItem(descriptor).node
?: error("Expected node, but value found. Check your serializer!") ?: 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<VisionManager> { public companion object : PluginFactory<VisionManager> {
override val tag: PluginTag = PluginTag(name = "vision", group = PluginTag.DATAFORGE_GROUP) override val tag: PluginTag = PluginTag(name = "vision", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out VisionManager> = VisionManager::class override val type: KClass<out VisionManager> = VisionManager::class

View File

@ -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<NameToken, Job>()
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<Vision> = 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()
}
}

View File

@ -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<VisionSerializationProxy> {
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<Vision> = 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()
}
}
}

View File

@ -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<in V : Vision, C : Renderer<V>> {
public fun attachRenderer(element: HTMLElement): C
}
/**
* Render a specific element and return container for configuration
*/
public fun <V : Vision, C : Renderer<V>> HTMLVisionDisplay<V, C>.render(element: HTMLElement, vision: V): C =
attachRenderer(element).apply { render(vision)}

View File

@ -27,7 +27,7 @@ import tornadofx.*
* *
* @author [Alexander Nozik](mailto:altavir@gmail.com) * @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 * Get or create a Node that could be later inserted into some parent

View File

@ -54,9 +54,9 @@ private class GDMLTransformer(val settings: GDMLTransformerSettings) {
} }
private val referenceStore = HashMap<Name, MutableList<Proxy>>() private val referenceStore = HashMap<Name, MutableList<SolidReference>>()
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 val templateName = solidsName + name
if (proto[templateName] == null) { if (proto[templateName] == null) {
solids.addSolid(root, solid, name) solids.addSolid(root, solid, name)
@ -66,7 +66,7 @@ private class GDMLTransformer(val settings: GDMLTransformerSettings) {
return ref 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() val templateName = volumesName + volume.name.asName()
if (proto[templateName] == null) { if (proto[templateName] == null) {
proto[templateName] = volume(root, volume) proto[templateName] = volume(root, volume)

View File

@ -1,14 +1,12 @@
package hep.dataforge.vision.gdml package hep.dataforge.vision.gdml
import hep.dataforge.vision.solid.AbstractProxy
import hep.dataforge.vision.solid.prototype import hep.dataforge.vision.solid.prototype
import hep.dataforge.vision.visitor.countDistinct import hep.dataforge.vision.visitor.countDistinct
import hep.dataforge.vision.visitor.countDistinctBy
import hep.dataforge.vision.visitor.flowStatistics import hep.dataforge.vision.visitor.flowStatistics
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.adaptivity.xmlutil.StAXReader
import kscience.gdml.GDML import kscience.gdml.GDML
import nl.adaptivity.xmlutil.StAXReader
import java.io.File import java.io.File
import kotlin.reflect.KClass import kotlin.reflect.KClass

View File

@ -6,3 +6,10 @@ plugins {
val ktorVersion: String by rootProject.extra 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")
}

View File

@ -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<HtmlFragment> = ArrayList<HtmlFragment>()
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<HtmlFragment> = emptyList(),
) {
routing.createRouteFromPath(rootRoute).apply {
val plots = HashMap<String, Plot>()
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<HtmlFragment> = 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)

View File

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

View File

@ -29,7 +29,7 @@ kotlin {
} }
jsMain { jsMain {
dependencies { dependencies {
implementation(npm("three", "0.114.0")) implementation(npm("three", "0.122.0"))
implementation(npm("three-csg-ts", "1.0.1")) implementation(npm("three-csg-ts", "1.0.1"))
} }
} }

View File

@ -5,9 +5,9 @@ import hep.dataforge.meta.descriptors.NodeDescriptor
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.asName import hep.dataforge.names.asName
import hep.dataforge.names.plus import hep.dataforge.names.plus
import hep.dataforge.output.Renderer
import hep.dataforge.values.ValueType import hep.dataforge.values.ValueType
import hep.dataforge.values.asValue import hep.dataforge.values.asValue
import hep.dataforge.vision.Renderer
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.Vision.Companion.VISIBLE_KEY import hep.dataforge.vision.Vision.Companion.VISIBLE_KEY
import hep.dataforge.vision.enum import hep.dataforge.vision.enum
@ -73,11 +73,11 @@ public interface Solid : Vision {
item(SolidMaterial.MATERIAL_KEY.toString(), SolidMaterial.descriptor) 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.position != second.position) return false
if (first.rotation != second.rotation) return false if (first.rotation != second.rotation) return false
if (first.scale != second.scale) return false if (first.scale != second.scale) return false
@ -85,8 +85,8 @@ public interface Solid : Vision {
return true return true
} }
internal fun solidHashCode(solid: Solid): Int{ internal fun solidHashCode(solid: Solid): Int {
var result = + (solid.position?.hashCode() ?: 0) var result = +(solid.position?.hashCode() ?: 0)
result = 31 * result + (solid.rotation?.hashCode() ?: 0) result = 31 * result + (solid.rotation?.hashCode() ?: 0)
result = 31 * result + (solid.scale?.hashCode() ?: 0) result = 31 * result + (solid.scale?.hashCode() ?: 0)
result = 31 * result + (solid.properties?.hashCode() ?: 0) result = 31 * result + (solid.properties?.hashCode() ?: 0)
@ -104,8 +104,7 @@ public var Solid.layer: Int
config[LAYER_KEY] = value.asValue() config[LAYER_KEY] = value.asValue()
} }
public fun Renderer<Solid>.render(meta: Meta = Meta.EMPTY, action: SolidGroup.() -> Unit): Unit = public fun Renderer<Solid>.render(action: SolidGroup.() -> Unit): Unit = render(SolidGroup().apply(action))
render(SolidGroup().apply(action), meta)
// Common properties // Common properties

View File

@ -35,7 +35,7 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) {
private fun PolymorphicModuleBuilder<Solid>.solids() { private fun PolymorphicModuleBuilder<Solid>.solids() {
subclass(SolidGroup.serializer()) subclass(SolidGroup.serializer())
subclass(Proxy.serializer()) subclass(SolidReference.serializer())
subclass(Composite.serializer()) subclass(Composite.serializer())
subclass(Tube.serializer()) subclass(Tube.serializer())
subclass(Box.serializer()) subclass(Box.serializer())

View File

@ -9,7 +9,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlin.collections.set import kotlin.collections.set
public abstract class AbstractProxy : BasicSolid(), VisionGroup { public abstract class AbstractReference : BasicSolid(), VisionGroup {
public abstract val prototype: Solid public abstract val prototype: Solid
override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { 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 @Serializable
@SerialName("solid.proxy") @SerialName("solid.ref")
public class Proxy( public class SolidReference(
public val templateName: Name, public val templateName: Name,
) : AbstractProxy(), Solid { ) : AbstractReference(), Solid {
/** /**
* Recursively search for defined template in the parent * Recursively search for defined template in the parent
@ -67,15 +67,15 @@ public class Proxy(
private val propertyCache: HashMap<Name, Config> = HashMap() private val propertyCache: HashMap<Name, Config> = HashMap()
override val children: Map<NameToken, Proxy.ProxyChild> override val children: Map<NameToken, SolidReference.ReferenceChild>
get() = (prototype as? VisionGroup)?.children get() = (prototype as? VisionGroup)?.children
?.filter { !it.key.toString().startsWith("@") } ?.filter { !it.key.toString().startsWith("@") }
?.mapValues { ?.mapValues {
ProxyChild(it.key.asName()) ReferenceChild(it.key.asName())
} ?: emptyMap() } ?: emptyMap()
private fun childPropertyName(childName: Name, propertyName: Name): Name { 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 { 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 * 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). * (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 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<NameToken, Vision> override val children: Map<NameToken, Vision>
get() = (prototype as? VisionGroup)?.children get() = (prototype as? VisionGroup)?.children
?.filter { !it.key.toString().startsWith("@") } ?.filter { !it.key.toString().startsWith("@") }
?.mapValues { (key, _) -> ?.mapValues { (key, _) ->
ProxyChild(name + key.asName()) ReferenceChild(name + key.asName())
} ?: emptyMap() } ?: emptyMap()
override var properties: Config? override var properties: Config?
@ -108,12 +108,12 @@ public class Proxy(
if (value == null) { if (value == null) {
propertyCache.remove(name)?.also { propertyCache.remove(name)?.also {
//Removing listener if it is present //Removing listener if it is present
removeChangeListener(this@Proxy) removeChangeListener(this@SolidReference)
} }
} else { } else {
propertyCache[name] = value.also { propertyCache[name] = value.also {
onPropertyChange(this@Proxy) { propertyName -> onPropertyChange(this@SolidReference) { propertyName ->
this@Proxy.propertyChanged(childPropertyName(name, propertyName)) this@SolidReference.propertyChanged(childPropertyName(name, propertyName))
} }
} }
} }
@ -122,16 +122,16 @@ public class Proxy(
} }
public companion object { 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 public val Vision.prototype: Vision
get() = when (this) { get() = when (this) {
is AbstractProxy -> prototype is AbstractReference -> prototype
else -> this else -> this
} }
@ -141,16 +141,16 @@ public val Vision.prototype: Vision
public fun SolidGroup.ref( public fun SolidGroup.ref(
templateName: Name, templateName: Name,
name: String = "", 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, name: String,
obj: Solid, obj: Solid,
templateName: Name = name.toName(), templateName: Name = name.toName(),
): Proxy { ): SolidReference {
val existing = getPrototype(templateName) val existing = getPrototype(templateName)
if (existing == null) { if (existing == null) {
prototypes { prototypes {
@ -159,5 +159,5 @@ public fun SolidGroup.proxy(
} else if (existing != obj) { } else if (existing != obj) {
error("Can't add different prototype on top of existing one") error("Can't add different prototype on top of existing one")
} }
return ref(templateName, name) return this@ref.ref(templateName, name)
} }

View File

@ -1,6 +1,7 @@
package hep.dataforge.vision.solid.specifications package hep.dataforge.vision.solid.specifications
import hep.dataforge.meta.* import hep.dataforge.meta.*
import hep.dataforge.names.Name
public class Canvas3DOptions : Scheme() { public class Canvas3DOptions : Scheme() {
public var axes: Axes by spec(Axes, Axes.empty()) 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 maxWith: Number by number { maxSize }
public var maxHeight: Number by number { maxSize } public var maxHeight: Number by number { maxSize }
public var onSelect: ((Name?)->Unit)? = null
public companion object : SchemeSpec<Canvas3DOptions>(::Canvas3DOptions) public companion object : SchemeSpec<Canvas3DOptions>(::Canvas3DOptions)
} }

View File

@ -40,7 +40,7 @@ internal object RemoveSingleChild : VisualTreeTransform<SolidGroup>() {
override fun SolidGroup.transformInPlace() { override fun SolidGroup.transformInPlace() {
fun MutableVisionGroup.replaceChildren() { fun MutableVisionGroup.replaceChildren() {
children.forEach { (childName, parent) -> children.forEach { (childName, parent) ->
if (parent is Proxy) return@forEach //ignore refs if (parent is SolidReference) return@forEach //ignore refs
if (parent is MutableVisionGroup) { if (parent is MutableVisionGroup) {
parent.replaceChildren() parent.replaceChildren()
} }

View File

@ -5,8 +5,8 @@ import hep.dataforge.names.Name
import hep.dataforge.names.asName import hep.dataforge.names.asName
import hep.dataforge.vision.MutableVisionGroup import hep.dataforge.vision.MutableVisionGroup
import hep.dataforge.vision.VisionGroup import hep.dataforge.vision.VisionGroup
import hep.dataforge.vision.solid.Proxy
import hep.dataforge.vision.solid.SolidGroup import hep.dataforge.vision.solid.SolidGroup
import hep.dataforge.vision.solid.SolidReference
@DFExperimental @DFExperimental
internal object UnRef : VisualTreeTransform<SolidGroup>() { internal object UnRef : VisualTreeTransform<SolidGroup>() {
@ -17,7 +17,7 @@ internal object UnRef : VisualTreeTransform<SolidGroup>() {
counter.forEach { (key, value) -> counter.forEach { (key, value) ->
reducer[key] = (reducer[key] ?: 0) + 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 reducer[obj.templateName] = (reducer[obj.templateName] ?: 0) + 1
} }
@ -27,9 +27,9 @@ internal object UnRef : VisualTreeTransform<SolidGroup>() {
private fun MutableVisionGroup.unref(name: Name) { private fun MutableVisionGroup.unref(name: Name) {
(this as? SolidGroup)?.prototypes?.set(name, null) (this as? SolidGroup)?.prototypes?.set(name, null)
children.filter { (it.value as? Proxy)?.templateName == name }.forEach { (key, value) -> children.filter { (it.value as? SolidReference)?.templateName == name }.forEach { (key, value) ->
val proxy = value as Proxy val reference = value as SolidReference
val newChild = mergeChild(proxy, proxy.prototype) val newChild = mergeChild(reference, reference.prototype)
newChild.parent = null newChild.parent = null
set(key.asName(), newChild) // replace proxy with merged object set(key.asName(), newChild) // replace proxy with merged object
} }

View File

@ -58,8 +58,8 @@ class PropertyTest {
} }
@Test @Test
fun testProxyStyleProperty() { fun testReferenceStyleProperty() {
var box: Proxy? = null var box: SolidReference? = null
val group = SolidGroup{ val group = SolidGroup{
styleSheet { styleSheet {
set("testStyle") { set("testStyle") {

View File

@ -3,7 +3,6 @@ package hep.dataforge.vision.solid
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.toName import hep.dataforge.names.toName
import hep.dataforge.vision.MutableVisionGroup import hep.dataforge.vision.MutableVisionGroup
import hep.dataforge.vision.Vision
import hep.dataforge.vision.get import hep.dataforge.vision.get
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -12,13 +11,13 @@ import kotlin.test.assertEquals
/** /**
* Create and attach new proxied group * Create and attach new proxied group
*/ */
fun SolidGroup.proxyGroup( fun SolidGroup.refGroup(
name: String, name: String,
templateName: Name = name.toName(), templateName: Name = name.toName(),
block: MutableVisionGroup.() -> Unit block: MutableVisionGroup.() -> Unit
): Proxy { ): SolidReference {
val group = SolidGroup().apply(block) val group = SolidGroup().apply(block)
return proxy(name, group, templateName) return ref(name, group, templateName)
} }
@ -44,8 +43,8 @@ class SerializationTest {
z = -100 z = -100
} }
val group = SolidGroup{ val group = SolidGroup{
proxy("cube", cube) ref("cube", cube)
proxyGroup("pg", "pg.content".toName()){ refGroup("pg", "pg.content".toName()){
sphere(50){ sphere(50){
x = -100 x = -100
} }

View File

@ -1,5 +0,0 @@
package hep.dataforge.vision.solid.three
fun createDataControls() {
val dat = kotlinext.js.require("dat.gui")
}

View File

@ -1,12 +1,12 @@
package hep.dataforge.vision.solid.three package hep.dataforge.vision.solid.three
import hep.dataforge.context.Context import hep.dataforge.meta.getItem
import hep.dataforge.meta.* import hep.dataforge.meta.string
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.plus import hep.dataforge.names.plus
import hep.dataforge.names.toName import hep.dataforge.names.toName
import hep.dataforge.output.Renderer
import hep.dataforge.vision.Colors import hep.dataforge.vision.Colors
import hep.dataforge.vision.Renderer
import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.Solid
import hep.dataforge.vision.solid.specifications.* import hep.dataforge.vision.solid.specifications.*
import hep.dataforge.vision.solid.three.ThreeMaterials.HIGHLIGHT_MATERIAL import hep.dataforge.vision.solid.three.ThreeMaterials.HIGHLIGHT_MATERIAL
@ -41,28 +41,24 @@ import kotlin.math.sin
public class ThreeCanvas( public class ThreeCanvas(
public val three: ThreePlugin, public val three: ThreePlugin,
public val options: Canvas3DOptions, public val options: Canvas3DOptions,
public val onClick: ((Name?) -> Unit)? = null,
) : Renderer<Solid> { ) : Renderer<Solid> {
override val context: Context get() = three.context
public var content: Solid? = null
private set
private var root: Object3D? = null private var root: Object3D? = null
private val raycaster = Raycaster() private val raycaster = Raycaster()
private val mousePosition: Vector2 = Vector2() private val mousePosition: Vector2 = Vector2()
public val axes: AxesHelper = AxesHelper(options.axes.size.toInt()).apply { public var content: Solid? = null
visible = options.axes.visible 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) add(axes)
} }
public val camera: PerspectiveCamera = buildCamera(options.camera) public var camera: PerspectiveCamera = buildCamera(options.camera)
private set
private var picked: Object3D? = null private var picked: Object3D? = null
@ -97,7 +93,7 @@ public class ThreeCanvas(
element.addEventListener("mousedown", { element.addEventListener("mousedown", {
val picked = pick() val picked = pick()
onClick?.invoke(picked?.fullName()) options.onSelect?.invoke(picked?.fullName())
}, false) }, false)
val renderer = WebGLRenderer { antialias = true }.apply { 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 old root
clear() clear()
val object3D = three.buildObject3D(vision)
val object3D = three.buildObject3D(obj)
object3D.name = "@root" object3D.name = "@root"
scene.add(object3D) scene.add(object3D)
content = obj content = vision
root = object3D root = object3D
} }
@ -261,16 +256,3 @@ public class ThreeCanvas(
private const val SELECT_NAME = "@select" 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)

View File

@ -13,7 +13,7 @@ import info.laht.threekt.math.Vector3
/** /**
* An implementation of geometry builder for Three.js [BufferGeometry] * An implementation of geometry builder for Three.js [BufferGeometry]
*/ */
class ThreeGeometryBuilder : GeometryBuilder<BufferGeometry> { public class ThreeGeometryBuilder : GeometryBuilder<BufferGeometry> {
private val vertices = ArrayList<Point3D>() private val vertices = ArrayList<Point3D>()
private val faces = ArrayList<Face3>() private val faces = ArrayList<Face3>()

View File

@ -2,21 +2,28 @@ package hep.dataforge.vision.solid.three
import hep.dataforge.context.* import hep.dataforge.context.*
import hep.dataforge.meta.Meta import hep.dataforge.meta.Meta
import hep.dataforge.meta.empty
import hep.dataforge.meta.invoke
import hep.dataforge.names.* import hep.dataforge.names.*
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.rendering.HTMLVisionDisplay
import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.*
import hep.dataforge.vision.solid.specifications.Canvas3DOptions
import hep.dataforge.vision.visible import hep.dataforge.vision.visible
import info.laht.threekt.core.Object3D import info.laht.threekt.core.Object3D
import org.w3c.dom.HTMLElement
import kotlin.collections.set import kotlin.collections.set
import kotlin.reflect.KClass import kotlin.reflect.KClass
import info.laht.threekt.objects.Group as ThreeGroup import info.laht.threekt.objects.Group as ThreeGroup
public class ThreePlugin : AbstractPlugin() { public class ThreePlugin : AbstractPlugin(), HTMLVisionDisplay<Solid, ThreeCanvas> {
override val tag: PluginTag get() = Companion.tag override val tag: PluginTag get() = Companion.tag
public val solidManager: SolidManager by require(SolidManager)
private val objectFactories = HashMap<KClass<out Solid>, ThreeFactory<*>>() private val objectFactories = HashMap<KClass<out Solid>, ThreeFactory<*>>()
private val compositeFactory = ThreeCompositeFactory(this) private val compositeFactory = ThreeCompositeFactory(this)
private val proxyFactory = ThreeProxyFactory(this) private val refFactory = ThreeReferenceFactory(this)
init { init {
//Add specialized factories here //Add specialized factories here
@ -38,7 +45,7 @@ public class ThreePlugin : AbstractPlugin() {
public fun buildObject3D(obj: Solid): Object3D { public fun buildObject3D(obj: Solid): Object3D {
return when (obj) { return when (obj) {
is ThreeVision -> obj.render() is ThreeVision -> obj.render()
is Proxy -> proxyFactory(obj) is SolidReference -> refFactory(obj)
is SolidGroup -> { is SolidGroup -> {
val group = ThreeGroup() val group = ThreeGroup()
obj.children.forEach { (token, child) -> obj.children.forEach { (token, child) ->
@ -56,7 +63,7 @@ public class ThreePlugin : AbstractPlugin() {
updatePosition(obj) updatePosition(obj)
//obj.onChildrenChange() //obj.onChildrenChange()
obj.onPropertyChange(this) { name-> obj.onPropertyChange(this) { name ->
if ( if (
name.startsWith(Solid.POSITION_KEY) || name.startsWith(Solid.POSITION_KEY) ||
name.startsWith(Solid.ROTATION) || 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()) { // if (name.isEmpty()) {
// logger.error { "Children change with empty name on $group" } // logger.error { "Children change with empty name on $group" }
// return@onChildrenChange // 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<ThreePlugin> { public companion object : PluginFactory<ThreePlugin> {
override val tag: PluginTag = PluginTag("visual.three", PluginTag.DATAFORGE_GROUP) override val tag: PluginTag = PluginTag("visual.three", PluginTag.DATAFORGE_GROUP)
override val type: KClass<ThreePlugin> = ThreePlugin::class override val type: KClass<ThreePlugin> = 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) { internal operator fun Object3D.set(token: NameToken, object3D: Object3D) {
object3D.name = token.toString() object3D.name = token.toString()
add(object3D) add(object3D)

View File

@ -3,18 +3,18 @@ package hep.dataforge.vision.solid.three
import hep.dataforge.names.cutFirst import hep.dataforge.names.cutFirst
import hep.dataforge.names.firstOrNull import hep.dataforge.names.firstOrNull
import hep.dataforge.names.toName 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.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.BufferGeometry
import info.laht.threekt.core.Object3D import info.laht.threekt.core.Object3D
import info.laht.threekt.objects.Mesh import info.laht.threekt.objects.Mesh
import kotlin.reflect.KClass import kotlin.reflect.KClass
public class ThreeProxyFactory(public val three: ThreePlugin) : ThreeFactory<Proxy> { public class ThreeReferenceFactory(public val three: ThreePlugin) : ThreeFactory<SolidReference> {
private val cache = HashMap<Solid, Object3D>() private val cache = HashMap<Solid, Object3D>()
override val type: KClass<Proxy> = Proxy::class override val type: KClass<SolidReference> = SolidReference::class
private fun Object3D.replicate(): Object3D { private fun Object3D.replicate(): Object3D {
return when (this) { return when (this) {
@ -30,7 +30,7 @@ public class ThreeProxyFactory(public val three: ThreePlugin) : ThreeFactory<Pro
} }
} }
override fun invoke(obj: Proxy): Object3D { override fun invoke(obj: SolidReference): Object3D {
val template = obj.prototype val template = obj.prototype
val cachedObject = cache.getOrPut(template) { val cachedObject = cache.getOrPut(template) {
three.buildObject3D(template) three.buildObject3D(template)
@ -44,12 +44,12 @@ public class ThreeProxyFactory(public val three: ThreePlugin) : ThreeFactory<Pro
} }
obj.onPropertyChange(this) { name -> obj.onPropertyChange(this) { name ->
if (name.firstOrNull()?.body == PROXY_CHILD_PROPERTY_PREFIX) { if (name.firstOrNull()?.body == REFERENCE_CHILD_PROPERTY_PREFIX) {
val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for proxy child property: '$name'") val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for reference child property: '$name'")
val propertyName = name.cutFirst() 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") val child = object3D.findChild(childName) ?: error("Object child with name '$childName' not found")
child.updateProperty(proxyChild, propertyName) child.updateProperty(referenceChild, propertyName)
} else { } else {
object3D.updateProperty(obj, name) object3D.updateProperty(obj, name)
} }

View File

@ -14,15 +14,15 @@ import info.laht.threekt.objects.Mesh
import info.laht.threekt.textures.Texture import info.laht.threekt.textures.Texture
import kotlin.math.PI 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 internal fun Double.toRadians() = this * PI / 180
fun CSG.toGeometry(): Geometry { public fun CSG.toGeometry(): Geometry {
val geom = Geometry() val geom = Geometry()
val vertices = ArrayList<Vector3>() val vertices = ArrayList<Vector3>()

View File

@ -28,7 +28,7 @@ class FX3DPlugin : AbstractPlugin() {
private val objectFactories = HashMap<KClass<out Solid>, FX3DFactory<*>>() private val objectFactories = HashMap<KClass<out Solid>, FX3DFactory<*>>()
private val compositeFactory = FXCompositeFactory(this) private val compositeFactory = FXCompositeFactory(this)
private val proxyFactory = FXProxyFactory(this) private val referenceFactory = FXReferenceFactory(this)
init { init {
//Add specialized factories here //Add specialized factories here
@ -45,7 +45,7 @@ class FX3DPlugin : AbstractPlugin() {
fun buildNode(obj: Solid): Node { fun buildNode(obj: Solid): Node {
val binding = VisualObjectFXBinding(obj) val binding = VisualObjectFXBinding(obj)
return when (obj) { return when (obj) {
is Proxy -> proxyFactory(obj, binding) is SolidReference -> referenceFactory(obj, binding)
is SolidGroup -> { is SolidGroup -> {
Group(obj.children.mapNotNull { (token, obj) -> Group(obj.children.mapNotNull { (token, obj) ->
(obj as? Solid)?.let { (obj as? Solid)?.let {

View File

@ -2,9 +2,8 @@ package hep.dataforge.vision.solid.fx
import hep.dataforge.context.Context import hep.dataforge.context.Context
import hep.dataforge.context.ContextAware import hep.dataforge.context.ContextAware
import hep.dataforge.meta.Meta
import hep.dataforge.meta.empty 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.Solid
import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.solid.specifications.Canvas3DOptions
import javafx.application.Platform import javafx.application.Platform
@ -81,7 +80,7 @@ class FXCanvas3D(val plugin: FX3DPlugin, val spec: Canvas3DOptions = Canvas3DOpt
} }
} }
override fun render(obj: Solid, meta: Meta) { override fun render(vision: Solid) {
rootObject = obj rootObject = vision
} }
} }

View File

@ -2,25 +2,25 @@ package hep.dataforge.vision.solid.fx
import hep.dataforge.names.* import hep.dataforge.names.*
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.solid.Proxy import hep.dataforge.vision.solid.SolidReference
import javafx.scene.Group import javafx.scene.Group
import javafx.scene.Node import javafx.scene.Node
import kotlin.reflect.KClass import kotlin.reflect.KClass
class FXProxyFactory(val plugin: FX3DPlugin) : FX3DFactory<Proxy> { class FXReferenceFactory(val plugin: FX3DPlugin) : FX3DFactory<SolidReference> {
override val type: KClass<in Proxy> get() = Proxy::class override val type: KClass<in SolidReference> get() = SolidReference::class
override fun invoke(obj: Proxy, binding: VisualObjectFXBinding): Node { override fun invoke(obj: SolidReference, binding: VisualObjectFXBinding): Node {
val prototype = obj.prototype val prototype = obj.prototype
val node = plugin.buildNode(prototype) val node = plugin.buildNode(prototype)
obj.onPropertyChange(this) { name-> obj.onPropertyChange(this) { name->
if (name.firstOrNull()?.body == Proxy.PROXY_CHILD_PROPERTY_PREFIX) { if (name.firstOrNull()?.body == SolidReference.REFERENCE_CHILD_PROPERTY_PREFIX) {
val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for proxy child property: '$name'") val childName = name.firstOrNull()?.index?.toName() ?: error("Wrong syntax for reference child property: '$name'")
val propertyName = name.cutFirst() 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") val child = node.findChild(childName) ?: error("Object child with name '$childName' not found")
child.updateProperty(proxyChild, propertyName) child.updateProperty(referenceChild, propertyName)
} }
} }
return node return node