diff --git a/demo/gdml/src/jsMain/kotlin/hep/dataforge/vision/gdml/demo/GDMLAppComponent.kt b/demo/gdml/src/jsMain/kotlin/hep/dataforge/vision/gdml/demo/GDMLAppComponent.kt index f2c4864c..3df7d3ca 100644 --- a/demo/gdml/src/jsMain/kotlin/hep/dataforge/vision/gdml/demo/GDMLAppComponent.kt +++ b/demo/gdml/src/jsMain/kotlin/hep/dataforge/vision/gdml/demo/GDMLAppComponent.kt @@ -1,6 +1,7 @@ package hep.dataforge.vision.gdml.demo import hep.dataforge.context.Context +import hep.dataforge.meta.invoke import hep.dataforge.names.Name import hep.dataforge.vision.Vision import hep.dataforge.vision.bootstrap.card @@ -12,6 +13,7 @@ import hep.dataforge.vision.react.flexColumn import hep.dataforge.vision.react.flexRow import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.SolidManager +import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.solid.three.ThreeCanvas import kotlinx.browser.window import kotlinx.css.* @@ -104,7 +106,9 @@ val GDMLApp = functionalComponent("GDMLApp") { props -> this.context = props.context this.obj = vision as? Solid this.selected = selected - this.clickCallback = onSelect + this.options = Canvas3DOptions.invoke{ + this.onSelect = onSelect + } this.canvasCallback = { canvas = it } diff --git a/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/GdmlFxDemoApp.kt b/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/GdmlFxDemoApp.kt index 0ccf3c70..de6968a7 100644 --- a/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/GdmlFxDemoApp.kt +++ b/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vision/gdml/demo/GdmlFxDemoApp.kt @@ -1,12 +1,11 @@ package hep.dataforge.vision.gdml.demo import hep.dataforge.context.Global +import hep.dataforge.vision.VisionManager import hep.dataforge.vision.editor.VisualObjectEditorFragment import hep.dataforge.vision.editor.VisualObjectTreeFragment import hep.dataforge.vision.gdml.toVision -import hep.dataforge.vision.VisionManager import hep.dataforge.vision.solid.Solid -import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.SolidMaterial import hep.dataforge.vision.solid.fx.FX3DPlugin import hep.dataforge.vision.solid.fx.FXCanvas3D @@ -27,7 +26,7 @@ class GDMLView : View() { } private val propertyEditor = VisualObjectEditorFragment { - it.getAllProperties() + it.allProperties }.apply { descriptorProperty.set(SolidMaterial.descriptor) itemProperty.bind(treeFragment.selectedProperty) diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt index aeb8d815..52c23c69 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt @@ -51,7 +51,7 @@ val MMApp = functionalComponent("Muon monitor") { props -> var selected by useState { props.selected } var canvas: ThreeCanvas? by useState { null } - val select: (Name?) -> Unit = { + val onSelect: (Name?) -> Unit = { selected = it } @@ -72,7 +72,7 @@ val MMApp = functionalComponent("Muon monitor") { props -> } //tree card("Object tree") { - objectTree(root, selected, select) + objectTree(root, selected, onSelect) } } styledDiv { @@ -85,9 +85,10 @@ val MMApp = functionalComponent("Muon monitor") { props -> attrs { this.context = props.context this.obj = root - this.options = canvasConfig + this.options = canvasConfig.apply { + this.onSelect = onSelect + } this.selected = selected - this.clickCallback = select this.canvasCallback = { canvas = it } @@ -176,7 +177,7 @@ val MMApp = functionalComponent("Muon monitor") { props -> configEditor( selectedObject.config, selectedObject.descriptor, - default = selectedObject.getAllProperties(), + default = selectedObject.allProperties, key = selected ) } diff --git a/demo/spatial-showcase/build.gradle.kts b/demo/spatial-showcase/build.gradle.kts index 87140138..cd0a131f 100644 --- a/demo/spatial-showcase/build.gradle.kts +++ b/demo/spatial-showcase/build.gradle.kts @@ -1,4 +1,6 @@ -import ru.mipt.npm.gradle.* +import ru.mipt.npm.gradle.DependencyConfiguration +import ru.mipt.npm.gradle.FXModule +import ru.mipt.npm.gradle.useFx plugins { id("ru.mipt.npm.mpp") @@ -23,8 +25,13 @@ kotlin { sourceSets { commonMain { dependencies { - api(project(":visionforge-solid")) - api(project(":visionforge-gdml")) + implementation(project(":visionforge-solid")) + implementation(project(":visionforge-gdml")) + } + } + jsMain{ + dependencies { + implementation("org.jetbrains:kotlin-css:1.0.0-pre.129-kotlin-1.4.10") } } } diff --git a/demo/spatial-showcase/src/commonMain/kotlin/hep/dataforge/vision/solid/demo/demo.kt b/demo/spatial-showcase/src/commonMain/kotlin/hep/dataforge/vision/solid/demo/demo.kt index 4f96f21b..1e962957 100644 --- a/demo/spatial-showcase/src/commonMain/kotlin/hep/dataforge/vision/solid/demo/demo.kt +++ b/demo/spatial-showcase/src/commonMain/kotlin/hep/dataforge/vision/solid/demo/demo.kt @@ -3,9 +3,8 @@ package hep.dataforge.vision.solid.demo import hep.dataforge.meta.Meta import hep.dataforge.meta.invoke import hep.dataforge.names.toName -import hep.dataforge.output.OutputManager import hep.dataforge.vision.Colors -import hep.dataforge.vision.Vision +import hep.dataforge.vision.layout.Page import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.visible @@ -16,12 +15,12 @@ import kotlin.math.sin import kotlin.random.Random -fun OutputManager.demo(name: String, title: String = name, block: SolidGroup.() -> Unit) { +fun Page.demo(name: String, title: String = name, block: SolidGroup.() -> Unit) { val meta = Meta { "title" put title } - val output = get(Vision::class, name.toName(), meta = meta) - output.render (action = block) + val output = output(name.toName(), meta)?: error("Output with name $name not found") + output.solidGroup (builder = block) } val canvasOptions = Canvas3DOptions { @@ -36,7 +35,7 @@ val canvasOptions = Canvas3DOptions { } } -fun OutputManager.showcase() { +fun Page.showcase() { demo("shapes", "Basic shapes") { box(100.0, 100.0, 100.0) { z = -110.0 @@ -133,7 +132,7 @@ fun OutputManager.showcase() { } } -fun OutputManager.showcaseCSG() { +fun Page.showcaseCSG() { demo("CSG.simple", "CSG operations") { composite(CompositeType.UNION) { box(100, 100, 100) { diff --git a/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/ThreeDemoApp.kt b/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/ThreeDemoApp.kt index 0bcd047b..206dbb2b 100644 --- a/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/ThreeDemoApp.kt +++ b/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/ThreeDemoApp.kt @@ -15,7 +15,7 @@ private class ThreeDemoApp : Application { override fun start(state: Map) { - val element = document.getElementById("canvas") ?: error("Element with id 'canvas' not found on page") + val element = document.getElementById("demo") ?: error("Element with id 'demo' not found on page") ThreeDemoGrid(element).run { showcase() 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 8ea0462d..a12355a4 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,57 +5,75 @@ import hep.dataforge.meta.Meta import hep.dataforge.meta.get import hep.dataforge.meta.string import hep.dataforge.names.Name -import hep.dataforge.output.Renderer -import hep.dataforge.vision.Vision +import hep.dataforge.vision.layout.Output +import hep.dataforge.vision.layout.Page +import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreePlugin -import hep.dataforge.vision.solid.three.attachRenderer -import kotlinx.browser.document import kotlinx.dom.clear import kotlinx.html.dom.append -import kotlinx.html.dom.create -import kotlinx.html.h2 -import kotlinx.html.hr import kotlinx.html.id -import kotlinx.html.js.div -import kotlinx.html.span +import kotlinx.html.js.* +import kotlinx.html.role import org.w3c.dom.Element -import kotlin.reflect.KClass +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.HTMLElement -class ThreeDemoGrid(element: Element, meta: Meta = Meta.EMPTY) { +class ThreeDemoGrid(element: Element, idPrefix: String = "") : Page { + + + private lateinit var navigationElement: HTMLElement + private lateinit var contentElement: HTMLDivElement - private val gridRoot = document.create.div("row") private val outputs: MutableMap = HashMap() private val three = Global.plugins.fetch(ThreePlugin) init { element.clear() - element.append(gridRoot) - } - - @Suppress("UNCHECKED_CAST") - override fun get(type: KClass, name: Name, stage: Name, meta: Meta): Renderer { - - return outputs.getOrPut(name) { - if (type != Vision::class) error("Supports only DisplayObject") - lateinit var output: ThreeCanvas - //TODO calculate cell width here using jquery - gridRoot.append { - span("border") { - div("col-6") { - div { id = "output-$name" }.also { - output = three.attachRenderer(it, canvasOptions) - //output.attach(it) - } - hr() - h2 { +(meta["title"].string ?: name.toString()) } - } + element.append { + div("container") { + navigationElement = ul("nav nav-tabs") { + id = "${idPrefix}Tab" + role = "tablist" + } + contentElement = div("tab-content") { + id = "${idPrefix}TabContent" } } + } + } - output - } as Renderer + + @Suppress("UNCHECKED_CAST") + override fun output(name: Name, meta: Meta): Output = outputs.getOrPut(name) { + lateinit var output: ThreeCanvas + navigationElement.append { + li("nav-item") { + a(classes = "nav-link") { + id = "tab[$name]" + attributes["data-toggle"] = "tab" + href = "#$name" + role = "tab" + attributes["aria-controls"] = "$name" + attributes["aria-selected"] = "false" + +name.toString() + } + } + } + contentElement.append { + div("tab-pane fade col h-100") { + id = name.toString() + role = "tabpanel" + attributes["aria-labelledby"] = "tab[$name]" + div("container w-100 h-100") { id = "output-$name" }.also {element-> + output = three.createCanvas(element, canvasOptions) + } + hr() + h2 { +(meta["title"].string ?: name.toString()) } + } + } + output } } diff --git a/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/VariableBox.kt b/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/VariableBox.kt index 08830f35..8df28ddc 100644 --- a/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/VariableBox.kt +++ b/demo/spatial-showcase/src/jsMain/kotlin/hep/dataforge/vision/solid/demo/VariableBox.kt @@ -13,6 +13,7 @@ import hep.dataforge.vision.set import hep.dataforge.vision.setProperty import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.Solid.Companion.GEOMETRY_KEY +import hep.dataforge.vision.solid.SolidMaterial.Companion.MATERIAL_COLOR_KEY import hep.dataforge.vision.solid.three.* import hep.dataforge.vision.solid.three.ThreeMaterials.getMaterial import info.laht.threekt.core.BufferGeometry @@ -75,6 +76,9 @@ internal class VariableBox(xSize: Number, ySize: Number, zSize: Number) : ThreeV } name.startsWith(MeshThreeFactory.WIREFRAME_KEY) -> mesh.applyWireFrame(this@VariableBox) name.startsWith(MeshThreeFactory.EDGES_KEY) -> mesh.applyEdges(this@VariableBox) + name.startsWith(MATERIAL_COLOR_KEY)->{ + mesh.material = getMaterial(this, true) + } else -> mesh.updateProperty(this@VariableBox, name) } } diff --git a/demo/spatial-showcase/src/jsMain/resources/index.html b/demo/spatial-showcase/src/jsMain/resources/index.html index 278366e0..f3f04915 100644 --- a/demo/spatial-showcase/src/jsMain/resources/index.html +++ b/demo/spatial-showcase/src/jsMain/resources/index.html @@ -4,14 +4,25 @@ Three js demo for particle physics - + + +

Demo grid

-
+
+ + + + + \ No newline at end of file diff --git a/demo/spatial-showcase/src/jvmMain/kotlin/hep/dataforge/vision/solid/demo/FXDemoGrid.kt b/demo/spatial-showcase/src/jvmMain/kotlin/hep/dataforge/vision/solid/demo/FXDemoGrid.kt index 3fb8c1e5..ec0d5b0f 100644 --- a/demo/spatial-showcase/src/jvmMain/kotlin/hep/dataforge/vision/solid/demo/FXDemoGrid.kt +++ b/demo/spatial-showcase/src/jvmMain/kotlin/hep/dataforge/vision/solid/demo/FXDemoGrid.kt @@ -3,18 +3,17 @@ package hep.dataforge.vision.solid.demo import hep.dataforge.context.Global import hep.dataforge.meta.Meta import hep.dataforge.names.Name -import hep.dataforge.output.OutputManager -import hep.dataforge.output.Renderer -import hep.dataforge.vision.Vision +import hep.dataforge.vision.layout.Output +import hep.dataforge.vision.layout.Page +import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.fx.FX3DPlugin import hep.dataforge.vision.solid.fx.FXCanvas3D import javafx.collections.FXCollections import javafx.scene.Parent import javafx.scene.control.Tab import tornadofx.* -import kotlin.reflect.KClass -class FXDemoGrid : View(title = "DataForge-vis FX demo"), OutputManager { +class FXDemoGrid : View(title = "DataForge-vis FX demo"), Page { private val outputs = FXCollections.observableHashMap() override val root: Parent = borderpane { @@ -27,14 +26,8 @@ class FXDemoGrid : View(title = "DataForge-vis FX demo"), OutputManager { private val fx3d = Global.plugins.fetch(FX3DPlugin) - @Suppress("UNCHECKED_CAST") - override fun get(type: KClass, name: Name, stage: Name, meta: Meta): Renderer { - return outputs.getOrPut(name) { - if (type != Vision::class) kotlin.error("Supports only DisplayObject") - val output = FXCanvas3D(fx3d, canvasOptions) - - output - } as Renderer + override fun output(name: Name, meta: Meta): Output = outputs.getOrPut(name) { + FXCanvas3D(fx3d, canvasOptions) } } \ No newline at end of file diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt index ac0b5c33..839c35d3 100644 --- a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt @@ -54,7 +54,7 @@ public val ThreeControls: FunctionalComponent = functionalCo if (selectedObject != null) { visionPropertyEditor( selectedObject, - default = selectedObject.getAllProperties(), + default = selectedObject.allProperties, key = selected ) } 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 df52938d..bd0c8797 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 @@ -19,7 +19,6 @@ public external interface ThreeCanvasProps : RProps { public var obj: Solid? public var options: Canvas3DOptions? public var selected: Name? - public var clickCallback: (Name?) -> Unit public var canvasCallback: ((ThreeCanvas?) -> Unit)? } @@ -39,7 +38,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.attachRenderer(element, props.options ?: Canvas3DOptions.empty(), props.clickCallback) + three.createCanvas(element, props.options ?: Canvas3DOptions.empty()) props.canvasCallback?.invoke(newCanvas) canvas = newCanvas } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Renderer.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Renderer.kt deleted file mode 100644 index 820ac615..00000000 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Renderer.kt +++ /dev/null @@ -1,5 +0,0 @@ -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/Vision.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt index 3a9cc81c..1ee5036e 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt @@ -12,8 +12,6 @@ import hep.dataforge.vision.Vision.Companion.TYPE import hep.dataforge.vision.Vision.Companion.VISIBLE_KEY import kotlinx.serialization.PolymorphicSerializer import kotlinx.serialization.Transient -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty /** * A root type for display hierarchy @@ -35,7 +33,7 @@ public interface Vision : Configurable, Described { /** * All properties including styles and prototypes if present, including inherited ones */ - public fun getAllProperties(): Laminate + public val allProperties: Laminate /** * Get property (including styles). [inherit] toggles parent node property lookup @@ -116,27 +114,27 @@ public var Vision.visible: Boolean? get() = getProperty(VISIBLE_KEY).boolean set(value) = config.setValue(VISIBLE_KEY, value?.asValue()) -/** - * Convinience delegate for properties - */ -public fun Vision.property( - default: MetaItem<*>? = null, - key: Name? = null, - inherit: Boolean = true, -): MutableItemDelegate = - object : ReadWriteProperty?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*>? { - val name = key ?: property.name.toName() - return getProperty(name, inherit) ?: default - } +///** +// * Convenience delegate for properties +// */ +//public fun Vision.property( +// default: MetaItem<*>? = null, +// key: Name? = null, +// inherit: Boolean = true, +//): MutableItemDelegate = +// object : ReadWriteProperty?> { +// override fun getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*>? { +// val name = key ?: property.name.toName() +// return getProperty(name, inherit) ?: default +// } +// +// override fun setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>?) { +// val name = key ?: property.name.toName() +// setProperty(name, value) +// } +// } - override fun setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>?) { - val name = key ?: property.name.toName() - setProperty(name, value) - } - } - -public fun Vision.properties(inherit: Boolean = true): MutableItemProvider = object : MutableItemProvider { +public fun Vision.props(inherit: Boolean = true): MutableItemProvider = object : MutableItemProvider { override fun getItem(name: Name): MetaItem<*>? { return getProperty(name, inherit) } 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 8a2918a1..492fee47 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt @@ -3,6 +3,7 @@ package hep.dataforge.vision import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.meta.descriptors.defaultItem +import hep.dataforge.meta.descriptors.defaultMeta import hep.dataforge.meta.descriptors.get import hep.dataforge.names.Name import hep.dataforge.names.asName @@ -75,24 +76,22 @@ public open class VisionBase : Vision { /** * All available properties in a layered form */ - override fun getAllProperties(): Laminate = Laminate(properties, allStyles, parent?.getAllProperties()) + override val allProperties: Laminate + get() = Laminate( + properties, + allStyles, + parent?.allProperties, + descriptor?.defaultMeta(), + ) - override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { - return if (inherit) { - sequence { - yield(properties?.get(name)) - yieldAll(getStyleItems(name)) - yield(descriptor?.get(name)?.defaultItem()) - yield(parent?.getProperty(name, inherit)) - }.merge() - } else { - sequence { - yield(properties?.get(name)) - yieldAll(getStyleItems(name)) - yield(descriptor?.get(name)?.defaultItem()) - }.merge() + override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = sequence { + yield(properties?.get(name)) + yieldAll(getStyleItems(name)) + if (inherit) { + yield(parent?.getProperty(name, inherit)) } - } + yield(descriptor?.get(name)?.defaultItem()) + }.merge() /** * Reset all properties to their default values 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 426644ca..14a72d2a 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt @@ -19,17 +19,13 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) { public val serializersModule: SerializersModule get() = SerializersModule { include(defaultSerialModule) - context.gather(VISION_SERIAL_MODULE_TARGET).values.forEach { + context.gather(VISION_SERIALIZER_MODULE_TARGET).values.forEach { include(it) } } public val jsonFormat: Json - get() = Json { - prettyPrint = true - useArrayPolymorphism = false - encodeDefaults = false - ignoreUnknownKeys = true + get() = Json(defaultJson) { serializersModule = this@VisionManager.serializersModule } @@ -52,14 +48,23 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) { override val tag: PluginTag = PluginTag(name = "vision", group = PluginTag.DATAFORGE_GROUP) override val type: KClass = VisionManager::class - public const val VISION_SERIAL_MODULE_TARGET: String = "visionSerialModule" + public const val VISION_SERIALIZER_MODULE_TARGET: String = "visionSerializerModule" override fun invoke(meta: Meta, context: Context): VisionManager = VisionManager(meta) private val defaultSerialModule: SerializersModule = SerializersModule { polymorphic(Vision::class) { + subclass(VisionBase.serializer()) subclass(VisionGroupBase.serializer()) } } + + public val defaultJson: Json = Json { + serializersModule = defaultSerialModule + prettyPrint = true + useArrayPolymorphism = false + encodeDefaults = false + ignoreUnknownKeys = true + } } } \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/BindingHtmlOutputScope.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/BindingHtmlOutputScope.kt new file mode 100644 index 00000000..069d5635 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/BindingHtmlOutputScope.kt @@ -0,0 +1,18 @@ +package hep.dataforge.vision.html + +import hep.dataforge.names.Name +import hep.dataforge.vision.Vision +import kotlinx.html.TagConsumer + +public class BindingHtmlOutputScope( + root: TagConsumer, + prefix: String? = null, +) : HtmlOutputScope(root,prefix) { + + private val _bindings = HashMap() + public val bindings: Map get() = _bindings + + override fun renderVision(htmlOutput: HtmlOutput, vision: V) { + _bindings[htmlOutput.name] = vision + } +} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlOutputScope.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlOutputScope.kt new file mode 100644 index 00000000..28bd6c49 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlOutputScope.kt @@ -0,0 +1,52 @@ +package hep.dataforge.vision.html + +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import hep.dataforge.vision.Vision +import kotlinx.html.DIV +import kotlinx.html.TagConsumer +import kotlinx.html.div +import kotlinx.html.id + +public class HtmlOutput( + public val outputScope: HtmlOutputScope<*, V>, + public val name: Name, + public val div: DIV, +) + +public abstract class HtmlOutputScope( + private val root: TagConsumer, + public val prefix: String? = null, +) : TagConsumer by root { + + public open fun resolveId(name: Name): String = (prefix ?: "output:") + name.toString() + + /** + * Create a placeholder but do not attach any [Vision] to it + */ + public inline fun TagConsumer.visionOutput( + name: Name, + crossinline block: HtmlOutput.() -> Unit = {}, + ): T = div { + this.id = resolveId(name) + @Suppress("UNCHECKED_CAST") + HtmlOutput(this@HtmlOutputScope, name, this).block() + } + + + public inline fun TagConsumer.visionOutput( + name: String, + crossinline block: HtmlOutput.() -> Unit = {}, + ): T = visionOutput(name.toName(), block) + + /** + * Create a placeholder and put a [Vision] in it + */ + public abstract fun renderVision(htmlOutput: HtmlOutput, vision: V) + + public fun TagConsumer.vision(name: Name, vision: V): Unit { + visionOutput(name) { + renderVision(this, vision) + } + } +} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlVisionFragment.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlVisionFragment.kt new file mode 100644 index 00000000..d0140d39 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlVisionFragment.kt @@ -0,0 +1,8 @@ +package hep.dataforge.vision.html + +import hep.dataforge.vision.Vision + +public class HtmlVisionFragment(public val layout: HtmlOutputScope.() -> Unit) + +public fun buildVisionFragment(visit: HtmlOutputScope.() -> Unit): HtmlVisionFragment = + HtmlVisionFragment(visit) diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/StaticHtmlOutputScope.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/StaticHtmlOutputScope.kt new file mode 100644 index 00000000..02aea7d3 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/StaticHtmlOutputScope.kt @@ -0,0 +1,28 @@ +package hep.dataforge.vision.html + +import hep.dataforge.vision.Vision +import kotlinx.html.FlowContent +import kotlinx.html.TagConsumer +import kotlinx.html.stream.createHTML + +public typealias HtmlVisionRenderer = FlowContent.(V) -> Unit + +public class StaticHtmlOutputScope( + root: TagConsumer, + prefix: String? = null, + private val render: HtmlVisionRenderer, +) : HtmlOutputScope(root, prefix) { + + override fun renderVision(htmlOutput: HtmlOutput, vision: V) { + htmlOutput.div.render(vision) + } +} + +public fun HtmlVisionFragment.renderToObject( + root: TagConsumer, + prefix: String? = null, + renderer: HtmlVisionRenderer, +): T = StaticHtmlOutputScope(root, prefix, renderer).apply(layout).finalize() + +public fun HtmlVisionFragment.renderToString(renderer: HtmlVisionRenderer): String = + renderToObject(createHTML(), null, renderer) \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/layout/Page.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/layout/Page.kt new file mode 100644 index 00000000..cd66a3d6 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/layout/Page.kt @@ -0,0 +1,16 @@ +package hep.dataforge.vision.layout + +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.vision.Vision + +public interface Output { + public fun render(vision: V) +} + +public interface Page { + public fun output(name: Name, meta: Meta = Meta.EMPTY): Output? +} + +public fun Page.render(name: Name, vision: V): Unit = + output(name)?.render(vision) ?: error("Could not resolve renderer for name $name") \ 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 index 21434e80..e0041bf5 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionChange.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionChange.kt @@ -28,7 +28,8 @@ public abstract class EmptyVision : Vision { override val properties: Config? = null - override fun getAllProperties(): Laminate = Laminate(Meta.EMPTY) + override val allProperties: Laminate + get() = Laminate(Meta.EMPTY) override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = null diff --git a/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt b/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt new file mode 100644 index 00000000..4c48fad7 --- /dev/null +++ b/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt @@ -0,0 +1,52 @@ +package hep.dataforge.vision.html + +import hep.dataforge.meta.configure +import hep.dataforge.meta.set +import hep.dataforge.vision.Vision +import hep.dataforge.vision.VisionBase +import hep.dataforge.vision.VisionGroup +import kotlinx.html.* +import kotlin.test.Test + +class HtmlTagTest { + + fun HtmlOutput.vision(block: Vision.() -> Unit) = + outputScope.renderVision(this, VisionBase().apply(block)) + + val fragment = buildVisionFragment { + div { + h1 { +"Head" } + visionOutput("ddd") { + vision { + configure { + set("myProp", 82) + } + } + } + } + } + + val simpleVisionRenderer: HtmlVisionRenderer = { vision -> + div { + h2 { +"Properties" } + ul { + vision.properties?.items?.forEach { + li { + a { +it.key.toString() } + p { +it.value.toString() } + } + } + } + } + } + + val groupRenderer: HtmlVisionRenderer = { group -> + p { +"This is group" } + } + + + @Test + fun testStringRender() { + println(fragment.renderToString(simpleVisionRenderer)) + } +} \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/html/DynamicHtmlOutputContext.kt b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/html/DynamicHtmlOutputContext.kt new file mode 100644 index 00000000..573055a0 --- /dev/null +++ b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/html/DynamicHtmlOutputContext.kt @@ -0,0 +1,31 @@ +package hep.dataforge.vision.html + +import hep.dataforge.vision.Vision +import kotlinx.browser.document +import kotlinx.html.TagConsumer +import org.w3c.dom.Element +import org.w3c.dom.HTMLElement + +public interface HtmlVisionBinding{ + public fun bind(element: Element, vision: V): Unit +} + +public fun Map.bind(binder: HtmlVisionBinding){ + forEach { (id, vision) -> + val element = document.getElementById(id) ?: error("Could not find element with id $id") + binder.bind(element, vision) + } +} + +public fun HtmlVisionFragment.bindToDocument( + root: TagConsumer, + binder: HtmlVisionBinding, +): HTMLElement = BindingHtmlOutputScope(root).apply(layout).let { scope -> + scope.finalize().apply { + scope.bindings.forEach { (name, vision) -> + val id = scope.resolveId(name) + val element = document.getElementById(id) ?: error("Could not find element with name $name and id $id") + binder.bind(element, vision) + } + } +} 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 deleted file mode 100644 index 58f2b2b1..00000000 --- a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/rendering/HTMLVisionDisplay.kt +++ /dev/null @@ -1,20 +0,0 @@ -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-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt index 98894710..32e3262a 100644 --- a/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt @@ -1,219 +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 -} - +//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() +// } +//} +// +// ///** -// * Start static server (updates via reload) +// * Attach plotly application to given server // */ -//@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 +//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 deleted file mode 100644 index d6df7edc..00000000 --- a/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/html.kt +++ /dev/null @@ -1,70 +0,0 @@ -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/src/commonMain/kotlin/hep/dataforge/vision/solid/PolyLine.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/PolyLine.kt index 957e4501..766eab08 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/PolyLine.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/PolyLine.kt @@ -7,7 +7,7 @@ import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.names.plus import hep.dataforge.vision.VisionContainerBuilder -import hep.dataforge.vision.properties +import hep.dataforge.vision.props import hep.dataforge.vision.set import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -18,7 +18,7 @@ import kotlinx.serialization.UseSerializers public class PolyLine(public var points: List) : BasicSolid(), Solid { //var lineType by string() - public var thickness: Number by properties().number(1.0, key = SolidMaterial.MATERIAL_KEY + THICKNESS_KEY) + public var thickness: Number by props().number(1.0, key = SolidMaterial.MATERIAL_KEY + THICKNESS_KEY) public companion object { public val THICKNESS_KEY: Name = "thickness".asName() diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Solid.kt index 024f5215..f08211a0 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 @@ -7,10 +7,10 @@ import hep.dataforge.names.asName import hep.dataforge.names.plus 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 +import hep.dataforge.vision.layout.Output import hep.dataforge.vision.setProperty import hep.dataforge.vision.solid.Solid.Companion.DETAIL_KEY import hep.dataforge.vision.solid.Solid.Companion.IGNORE_KEY @@ -104,7 +104,7 @@ public var Solid.layer: Int config[LAYER_KEY] = value.asValue() } -public fun Renderer.render(action: SolidGroup.() -> Unit): Unit = render(SolidGroup().apply(action)) +public fun Output.solidGroup(builder: SolidGroup.() -> Unit): Unit = render(SolidGroup().apply(builder)) // 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 319fffca..64b032d4 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 @@ -11,7 +11,7 @@ import hep.dataforge.vision.Vision import hep.dataforge.vision.VisionGroup import hep.dataforge.vision.VisionGroupBase import hep.dataforge.vision.VisionManager -import hep.dataforge.vision.VisionManager.Companion.VISION_SERIAL_MODULE_TARGET +import hep.dataforge.vision.VisionManager.Companion.VISION_SERIALIZER_MODULE_TARGET import kotlinx.serialization.json.Json import kotlinx.serialization.modules.* import kotlin.reflect.KClass @@ -24,7 +24,7 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) { override val tag: PluginTag get() = Companion.tag override fun content(target: String): Map = when (target) { - VISION_SERIAL_MODULE_TARGET -> mapOf(tag.name.toName() to serializersModuleForSolids) + VISION_SERIALIZER_MODULE_TARGET -> mapOf(tag.name.toName() to serializersModuleForSolids) else -> super.content(target) } @@ -60,11 +60,7 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) { } } - internal val jsonForSolids: Json = Json{ - prettyPrint = true - useArrayPolymorphism = false - encodeDefaults = false - ignoreUnknownKeys = true + internal val jsonForSolids: Json = Json(VisionManager.defaultJson){ serializersModule = serializersModuleForSolids } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidMaterial.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidMaterial.kt index 34a12916..c26300d1 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidMaterial.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidMaterial.kt @@ -54,7 +54,6 @@ public class SolidMaterial : Scheme() { NodeDescriptor { value(COLOR_KEY) { type(ValueType.STRING, ValueType.NUMBER) - default("#ffffff") widgetType = "color" } value(OPACITY_KEY) { diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt index 266366ce..cf652b8b 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidReference.kt @@ -12,22 +12,14 @@ import kotlin.collections.set public abstract class AbstractReference : BasicSolid(), VisionGroup { public abstract val prototype: Solid - override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { - return if (inherit) { - sequence { - yield(properties?.get(name)) - yieldAll(getStyleItems(name)) - yield(prototype.getProperty(name)) - yield(parent?.getProperty(name, inherit)) - }.merge() - } else { - sequence { - yield(properties?.get(name)) - yieldAll(getStyleItems(name)) - yield(prototype.getProperty(name, false)) - }.merge() + override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = sequence { + yield(properties?.get(name)) + yieldAll(getStyleItems(name)) + yield(prototype.getProperty(name)) + if (inherit) { + yield(parent?.getProperty(name, inherit)) } - } + }.merge() override var styles: List get() = (properties[Vision.STYLE_KEY]?.stringList ?: emptyList()) + prototype.styles @@ -35,8 +27,13 @@ public abstract class AbstractReference : BasicSolid(), VisionGroup { config[Vision.STYLE_KEY] = value } - override fun getAllProperties(): Laminate = - Laminate(properties, allStyles, prototype.getAllProperties(), parent?.getAllProperties()) + override val allProperties: Laminate + get() = Laminate( + properties, + allStyles, + prototype.allProperties, + parent?.allProperties, + ) override fun attachChildren() { //do nothing 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 834fcf70..ebdf950a 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 @@ -6,7 +6,7 @@ import hep.dataforge.names.Name import hep.dataforge.names.plus import hep.dataforge.names.toName import hep.dataforge.vision.Colors -import hep.dataforge.vision.Renderer +import hep.dataforge.vision.layout.Output import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.specifications.* import hep.dataforge.vision.solid.three.ThreeMaterials.HIGHLIGHT_MATERIAL @@ -24,12 +24,9 @@ import info.laht.threekt.materials.LineBasicMaterial import info.laht.threekt.math.Vector2 import info.laht.threekt.objects.LineSegments import info.laht.threekt.objects.Mesh -import info.laht.threekt.renderers.WebGLRenderer import info.laht.threekt.scenes.Scene -import kotlinx.browser.window -import kotlinx.dom.clear +import org.w3c.dom.Element import org.w3c.dom.HTMLCanvasElement -import org.w3c.dom.HTMLElement import org.w3c.dom.Node import org.w3c.dom.events.MouseEvent import kotlin.math.cos @@ -41,7 +38,7 @@ import kotlin.math.sin public class ThreeCanvas( public val three: ThreePlugin, public val options: Canvas3DOptions, -) : Renderer { +) : Output { private var root: Object3D? = null private val raycaster = Raycaster() @@ -62,55 +59,60 @@ public class ThreeCanvas( private var picked: Object3D? = null + private val renderer = WebGLRenderer { antialias = true }.apply { + setClearColor(Colors.skyblue, 1) + } + + public val canvas: HTMLCanvasElement = renderer.domElement as HTMLCanvasElement + + /** + * Force camera aspect ration and renderer size recalculation + */ + public fun updateSize() { + val width = canvas.clientWidth + val height = canvas.clientHeight + renderer.setSize(width, height, false) + camera.aspect = width.toDouble() / height.toDouble() + camera.updateProjectionMatrix() + } + /** * Attach canvas to given [HTMLElement] */ - public fun attach(element: HTMLElement) { - fun WebGLRenderer.resize() { - val canvas = domElement as HTMLCanvasElement - - val width = options.computeWidth(canvas.clientWidth) - val height = options.computeHeight(canvas.clientHeight) - - canvas.width = width - canvas.height = height - - setSize(width, height, false) - camera.aspect = width.toDouble() / height - camera.updateProjectionMatrix() - } - - element.clear() - - //Attach listener to track mouse changes - element.addEventListener("mousemove", { event -> - (event as? MouseEvent)?.run { - val rect = element.getBoundingClientRect() - mousePosition.x = ((event.clientX - rect.left) / element.clientWidth) * 2 - 1 - mousePosition.y = -((event.clientY - rect.top) / element.clientHeight) * 2 + 1 - } - }, false) - - element.addEventListener("mousedown", { + init { + canvas.addEventListener("pointerdown", { val picked = pick() options.onSelect?.invoke(picked?.fullName()) }, false) - val renderer = WebGLRenderer { antialias = true }.apply { - setClearColor(Colors.skyblue, 1) - } + //Attach listener to track mouse changes + canvas.addEventListener("mousemove", { event -> + (event as? MouseEvent)?.run { + val rect = canvas.getBoundingClientRect() + mousePosition.x = ((event.clientX - rect.left) / canvas.clientWidth) * 2 - 1 + mousePosition.y = -((event.clientY - rect.top) / canvas.clientHeight) * 2 + 1 + } + }, false) - val canvas = renderer.domElement as HTMLCanvasElement canvas.style.apply { width = "100%" + minWidth = "${options.minWith.toInt()}px" + maxWidth = "${options.maxWith.toInt()}px" height = "100%" + minHeight = "${options.minHeight.toInt()}px" + maxHeight = "${options.maxHeight.toInt()}px" display = "block" } - addControls(renderer.domElement, options.controls) - fun animate() { + canvas.onresize = { + updateSize() + } + + addControls(canvas, options.controls) + + renderer.setAnimationLoop { val picked = pick() if (picked != null && this.picked != picked) { @@ -119,21 +121,13 @@ public class ThreeCanvas( this.picked = picked } - window.requestAnimationFrame { - animate() - } - renderer.render(scene, camera) } + } - element.appendChild(renderer.domElement) - renderer.resize() - - element.onresize = { - renderer.resize() - } - - animate() + public fun attach(element: Element) { + element.appendChild(canvas) + updateSize() } /** diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeMaterials.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeMaterials.kt index 969e725f..8f1dcbfa 100644 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeMaterials.kt +++ b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeMaterials.kt @@ -41,7 +41,7 @@ public object ThreeMaterials { linewidth = meta["thickness"].double ?: 1.0 } - fun getLineMaterial(meta: Meta?, cache: Boolean): LineBasicMaterial { + public fun getLineMaterial(meta: Meta?, cache: Boolean): LineBasicMaterial { if (meta == null) return DEFAULT_LINE return if (cache) { lineMaterialCache.getOrPut(meta) { buildLineMaterial(meta) } @@ -73,7 +73,7 @@ public object ThreeMaterials { } } - fun getMaterial(vision3D: Vision, cache: Boolean): Material { + public fun getMaterial(vision3D: Vision, cache: Boolean): Material { val meta = vision3D.getProperty(SolidMaterial.MATERIAL_KEY).node ?: return DEFAULT return if (cache) { materialCache.getOrPut(meta) { buildMaterial(meta) } @@ -87,7 +87,7 @@ public object ThreeMaterials { /** * Infer color based on meta item */ -fun MetaItem<*>.getColor(): Color { +public fun MetaItem<*>.getColor(): Color { return when (this) { is MetaItem.ValueItem -> if (this.value.type == ValueType.NUMBER) { val int = value.number.toInt() 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 6cbdcc10..628f7804 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 @@ -6,17 +6,18 @@ 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.html.HtmlVisionBinding 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.Element 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(), HTMLVisionDisplay { +public class ThreePlugin : AbstractPlugin(), HtmlVisionBinding { override val tag: PluginTag get() = Companion.tag public val solidManager: SolidManager by require(SolidManager) @@ -115,10 +116,15 @@ public class ThreePlugin : AbstractPlugin(), HTMLVisionDisplay { @@ -128,16 +134,11 @@ public class ThreePlugin : AbstractPlugin(), HTMLVisionDisplay Unit = {}, -): ThreeCanvas = attachRenderer(element, Canvas3DOptions(options)).apply { render(obj) } +): ThreeCanvas = createCanvas(element, Canvas3DOptions(options)).apply { render(obj) } internal operator fun Object3D.set(token: NameToken, object3D: Object3D) { object3D.name = token.toString() diff --git a/visionforge-solid/src/jsMain/kotlin/info/laht/threekt/renderers/WebGLRenderer.kt b/visionforge-solid/src/jsMain/kotlin/info/laht/threekt/renderers/WebGLRenderer.kt index c46e941e..80edbbca 100644 --- a/visionforge-solid/src/jsMain/kotlin/info/laht/threekt/renderers/WebGLRenderer.kt +++ b/visionforge-solid/src/jsMain/kotlin/info/laht/threekt/renderers/WebGLRenderer.kt @@ -64,7 +64,7 @@ external class WebGLRenderer(params: WebGLRendererParams = definedExternally) { fun clear( color: Boolean = definedExternally, depth: Boolean = definedExternally, - stencil: Boolean = definedExternally + stencil: Boolean = definedExternally, ) fun clearColor() @@ -84,6 +84,11 @@ external class WebGLRenderer(params: WebGLRendererParams = definedExternally) { */ fun setClearColor(color: Int, alpha: Number) + /** + * @param callback The function will be called every available frame. If `null` is passed it will stop any already ongoing animation. + */ + fun setAnimationLoop(callback: () -> Unit) + /** * Render a scene using a camera. * The render is done to the renderTarget (if specified) or to the canvas as usual. @@ -94,7 +99,7 @@ external class WebGLRenderer(params: WebGLRendererParams = definedExternally) { scene: Scene, camera: Camera, renderTarget: dynamic = definedExternally, - forceClear: Boolean = definedExternally + forceClear: Boolean = definedExternally, ) fun setPixelRatio(value: Number) 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 e8989840..4abc02b7 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 @@ -3,7 +3,7 @@ package hep.dataforge.vision.solid.fx import hep.dataforge.context.Context import hep.dataforge.context.ContextAware import hep.dataforge.meta.empty -import hep.dataforge.vision.Renderer +import hep.dataforge.vision.layout.Output import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.specifications.Canvas3DOptions import javafx.application.Platform @@ -15,7 +15,7 @@ import org.fxyz3d.scene.Axes import tornadofx.* class FXCanvas3D(val plugin: FX3DPlugin, val spec: Canvas3DOptions = Canvas3DOptions.empty()) : - Fragment(), Renderer, ContextAware { + Fragment(), Output, ContextAware { override val context: Context get() = plugin.context