diff --git a/dataforge-vis-common/build.gradle.kts b/dataforge-vis-common/build.gradle.kts index 465b03c2..11236809 100644 --- a/dataforge-vis-common/build.gradle.kts +++ b/dataforge-vis-common/build.gradle.kts @@ -6,6 +6,10 @@ val dataforgeVersion: String by rootProject.extra //val kvisionVersion: String by rootProject.extra("2.0.0-M1") kotlin { + js { + useCommonJs() + } + sourceSets { commonMain { dependencies { diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/ConfigProperty.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/ConfigProperty.kt new file mode 100644 index 00000000..113d77ba --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/ConfigProperty.kt @@ -0,0 +1,24 @@ +package hep.dataforge.properties + +import hep.dataforge.meta.Config +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.get +import hep.dataforge.names.Name + +class ConfigProperty(val config: Config, val name: Name) : Property?> { + override var value: MetaItem<*>? + get() = config[name] + set(value) { + config[name] = value + } + + override fun onChange(owner: Any?, callback: (MetaItem<*>?) -> Unit) { + config.onChange(owner) { name, oldItem, newItem -> + if (name == this.name && oldItem != newItem) callback(newItem) + } + } + + override fun removeChangeListener(owner: Any?) { + config.removeListener(owner) + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/Property.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/Property.kt new file mode 100644 index 00000000..fdd37132 --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/properties/Property.kt @@ -0,0 +1,57 @@ +package hep.dataforge.properties + +import hep.dataforge.meta.DFExperimental +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +//TODO move to core + +interface Property { + var value: T + + fun onChange(owner: Any? = null, callback: (T) -> Unit) + fun removeChangeListener(owner: Any? = null) +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun Property.flow() = callbackFlow { + send(value) + onChange(this) { + //TODO add exception handler? + launch { + send(it) + } + } + awaitClose { removeChangeListener(this) } +} + +/** + * Reflect all changes in the [source] property onto this property + * + * @return a mirroring job + */ +fun Property.mirror(source: Property, scope: CoroutineScope): Job { + return scope.launch { + source.flow().collect { + value = it + } + } +} + +/** + * Bi-directional connection between properties + */ +@DFExperimental +fun Property.bind(other: Property) { + onChange(other) { + other.value = it + } + other.onChange { + this.value = it + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/AbstractVisualObject.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/AbstractVisualObject.kt index e355fa94..259a5e3a 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/AbstractVisualObject.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/AbstractVisualObject.kt @@ -1,9 +1,11 @@ package hep.dataforge.vis import hep.dataforge.meta.* +import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.values.Value +import hep.dataforge.values.ValueType import hep.dataforge.vis.VisualObject.Companion.STYLE_KEY import kotlinx.serialization.Transient @@ -19,15 +21,15 @@ abstract class AbstractVisualObject : VisualObject { protected abstract var properties: Config? - override var styles: List + final override var styles: List get() = properties?.get(STYLE_KEY).stringList set(value) { - //val allStyles = (field + value).distinct() setProperty(STYLE_KEY, Value.of(value)) updateStyles(value) } protected fun updateStyles(names: List) { + styleCache = null names.mapNotNull { findStyle(it) }.asSequence() .flatMap { it.items.asSequence() } .distinctBy { it.key } @@ -77,7 +79,7 @@ abstract class AbstractVisualObject : VisualObject { /** * All available properties in a layered form */ - override fun allProperties(): Laminate = Laminate(properties, mergedStyles) + override fun allProperties(): Laminate = Laminate(properties, mergedStyles, parent?.allProperties()) override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { return if (inherit) { @@ -86,6 +88,16 @@ abstract class AbstractVisualObject : VisualObject { properties?.get(name) ?: mergedStyles[name] } } + + companion object { + val descriptor = NodeDescriptor { + defineValue(STYLE_KEY){ + type(ValueType.STRING) + multiple = true + } + } + + } } //fun VisualObject.findStyle(styleName: Name): Meta? { diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bindings.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bindings.kt new file mode 100644 index 00000000..48d373d7 --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bindings.kt @@ -0,0 +1,30 @@ +package hep.dataforge.js + +import hep.dataforge.properties.Property +import org.w3c.dom.HTMLInputElement + +fun HTMLInputElement.bindValue(property: Property) { + if (this.onchange != null) error("Input element already bound") + this.onchange = { + property.value = this.value + Unit + } + property.onChange(this) { + if (value != it) { + value = it + } + } +} + +fun HTMLInputElement.bindChecked(property: Property) { + if (this.onchange != null) error("Input element already bound") + this.onchange = { + property.value = this.checked + Unit + } + property.onChange(this) { + if (checked != it) { + checked = it + } + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt index eb99704a..9c3b15f7 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt @@ -16,9 +16,11 @@ inline fun TagConsumer.card(title: String, crossinline block: TagCo } inline fun RBuilder.card(title: String, crossinline block: RBuilder.() -> Unit) { - div("card w-100") { + div("card w-100 h-100") { div("card-body") { - h3(classes = "card-title") { +title } + h3(classes = "card-title") { + +title + } block() } } @@ -78,7 +80,7 @@ fun RBuilder.accordion(id: String, elements: List. elements.forEachIndexed { index, (title, builder) -> val headerID = "${id}-${index}-heading" val collapseID = "${id}-${index}-collapse" - div("card") { + div("card p-0 m-0") { div("card-header") { attrs { this.id = headerID diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt index 3b04817b..1ed93464 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt @@ -1,10 +1,28 @@ package hep.dataforge.js -import react.RBuilder +import react.* import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty -fun RBuilder.initState(init: () -> T): ReadWriteProperty = +class RFBuilder : RBuilder() + +/** + * Get functional component from [func] + */ +fun

component( + func: RFBuilder.(props: P) -> Unit +): FunctionalComponent

{ + return { props: P -> + val nodes = RFBuilder().apply { func(props) }.childList + when (nodes.size) { + 0 -> null + 1 -> nodes.first() + else -> createElement(Fragment, kotlinext.js.js {}, *nodes.toTypedArray()) + } + } +} + +fun RFBuilder.state(init: () -> T): ReadWriteProperty = object : ReadWriteProperty { val pair = react.useState(init) override fun getValue(thisRef: Any?, property: KProperty<*>): T { @@ -16,3 +34,5 @@ fun RBuilder.initState(init: () -> T): ReadWriteProperty = } } +fun RFBuilder.memoize(vararg deps: dynamic, builder: () -> T): T = useMemo(builder, deps) + diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt index e283c92b..6da696f6 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt @@ -1,28 +1,22 @@ package hep.dataforge.vis.editor +import hep.dataforge.js.RFBuilder +import hep.dataforge.js.component +import hep.dataforge.js.state import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.* import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.plus -import hep.dataforge.values.* -import hep.dataforge.vis.widgetType -import kotlinx.html.ButtonType -import kotlinx.html.InputType import kotlinx.html.classes -import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onClickFunction import org.w3c.dom.Element -import org.w3c.dom.HTMLInputElement -import org.w3c.dom.HTMLSelectElement import org.w3c.dom.events.Event -import react.RBuilder -import react.RComponent -import react.RProps +import react.* import react.dom.* -import react.setState interface ConfigEditorProps : RProps { + /** * Root config object - always non null */ @@ -42,129 +36,83 @@ interface ConfigEditorProps : RProps { * Root descriptor */ var descriptor: NodeDescriptor? - } -class ConfigEditorComponent : RComponent() { +private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { + var expanded: Boolean by state { true } + val item = props.root[props.name] + val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name) + val defaultItem = props.default?.get(props.name) + val actualItem: MetaItem? = item ?: defaultItem ?: descriptorItem?.defaultItem() - override fun TreeState.init() { - expanded = true + val token = props.name.last()?.toString() ?: "Properties" + + var kostyl by state { false } + + fun update() { + kostyl = !kostyl } - override fun componentDidMount() { + useEffectWithCleanup(listOf(props.root)) { props.root.onChange(this) { name, _, _ -> if (name == props.name) { - forceUpdate() + update() } } + return@useEffectWithCleanup { props.root.removeListener(this) } } - override fun componentWillUnmount() { - props.root.removeListener(this) + val expanderClick: (Event) -> Unit = { + expanded = !expanded } - private val onClick: (Event) -> Unit = { - setState { - expanded = !expanded - } - } - - private val onValueChange: (Event) -> Unit = { - val value = when (val t = it.target) { - // (it.target as HTMLInputElement).value - is HTMLInputElement -> if (t.type == "checkbox") { - if (t.checked) True else False - } else { - t.value.asValue() - } - is HTMLSelectElement -> t.value.asValue() - else -> error("Unknown event target: $t") - } - try { - props.root.setValue(props.name, value) - } catch (ex: Exception) { - console.error("Can't set config property ${props.name} to $value") - } - } - - private val removeValue: (Event) -> Unit = { + val removeClick: (Event) -> Unit = { props.root.remove(props.name) + update() } - //TODO replace by separate components - private fun RBuilder.valueChooser(value: Value, descriptor: ValueDescriptor?) { - val type = descriptor?.type?.firstOrNull() - when { - type == ValueType.BOOLEAN -> { - input(type = InputType.checkBox, classes = "float-right") { + when (actualItem) { + is MetaItem.NodeItem -> { + div { + span("tree-caret") { attrs { - defaultChecked = value.boolean - onChangeFunction = onValueChange + if (expanded) { + classes += "tree-caret-down" + } + onClickFunction = expanderClick } } - } - type == ValueType.NUMBER -> input(type = InputType.number, classes = "float-right") { - attrs { - descriptor.attributes["step"].string?.let { - step = it - } - descriptor.attributes["min"].string?.let { - min = it - } - descriptor.attributes["max"].string?.let { - max = it - } - defaultValue = value.string - onChangeFunction = onValueChange - } - } - descriptor?.allowedValues?.isNotEmpty() ?: false -> select("float-right") { - descriptor!!.allowedValues.forEach { - option { - +it.string - } - } - attrs { - multiple = false - onChangeFunction = onValueChange - } - } - descriptor?.widgetType == "color" -> input(type = InputType.color, classes = "float-right") { - attrs { - defaultValue = value.string - onChangeFunction = onValueChange - } - } - else -> input(type = InputType.text, classes = "float-right") { - attrs { - defaultValue = value.string - onChangeFunction = onValueChange - } - } - } - - } - - - override fun RBuilder.render() { - val item = props.root[props.name] - val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name) - val defaultItem = props.default?.get(props.name) - val actualItem = item ?: defaultItem ?: descriptorItem?.defaultItem() - val token = props.name.last()?.toString() ?: "Properties" - - when (actualItem) { - is MetaItem.NodeItem -> { - div { - span("tree-caret") { - attrs { - if (state.expanded) { - classes += "tree-caret-down" - } - onClickFunction = onClick + span("tree-label") { + +token + attrs { + if (item == null) { + classes += "tree-label-inactive" } } - span("tree-label") { + } + } + if (expanded) { + ul("tree") { + val keys = buildSet { + (descriptorItem as? NodeDescriptor)?.items?.keys?.forEach { + add(NameToken(it)) + } + item?.node?.items?.keys?.let { addAll(it) } + defaultItem?.node?.items?.keys?.let { addAll(it) } + } + + keys.forEach { token -> + li("tree-item align-middle") { + configEditor(props.root, props.name + token, props.descriptor, props.default) + } + } + } + } + } + is MetaItem.ValueItem -> { + div { + div("d-flex flex-row align-items-center") { + div("flex-grow-1 p-1 mr-auto tree-label") { +token attrs { if (item == null) { @@ -172,109 +120,51 @@ class ConfigEditorComponent : RComponent() { } } } - } - if (state.expanded) { - ul("tree") { - val keys = buildSet { - item?.node?.items?.keys?.let { addAll(it) } - defaultItem?.node?.items?.keys?.let { addAll(it) } - (descriptorItem as? NodeDescriptor)?.items?.keys?.forEach { - add(NameToken(it)) - } - } - - keys.forEach { token -> - li("tree-item") { - child(ConfigEditorComponent::class) { - attrs { - this.root = props.root - this.name = props.name + token - this.default = props.default - this.descriptor = props.descriptor - } + div("d-inline-flex") { + valueChooser(props.root, props.name, actualItem.value, descriptorItem as? ValueDescriptor) + } + div("d-inline-flex p-1") { + button(classes = "btn btn-link") { + +"\u00D7" + attrs { + if (item == null) { + disabled = true + } else { + onClickFunction = removeClick } } } } } } - is MetaItem.ValueItem -> { - div { - div("row") { - div("col") { - p("tree-label") { - +token - attrs { - if (item == null) { - classes += "tree-label-inactive" - } - } - } - } - div("col") { - valueChooser(actualItem.value, descriptorItem as? ValueDescriptor) - } - div("col-auto") { - div("dropleft p-0") { - button(classes = "btn btn-outline-primary") { - attrs { - type = ButtonType.button - attributes["data-toggle"] = "dropdown" - attributes["aria-haspopup"] = "true" - attributes["aria-expanded"] = "false" - attributes["data-boundary"] = "viewport" - } - +"\u22ee" - } - div(classes = "dropdown-menu") { - button(classes = "btn btn-outline dropdown-item") { - +"Info" - } - if (item != null) { - button(classes = "btn btn-outline dropdown-item") { - +"""Clear""" - } - attrs { - onClickFunction = removeValue - } - } + } + } +} - } - } - } - } - } - } +val ConfigEditor: FunctionalComponent = component { configEditorItem(it) } + +fun RBuilder.configEditor( + config: Config, + name: Name = Name.EMPTY, + descriptor: NodeDescriptor? = null, + default: Meta? = null +) { + child(ConfigEditor) { + attrs { + this.root = config + this.name = name + this.descriptor = descriptor + this.default = default } } } fun Element.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null) { render(this) { - child(ConfigEditorComponent::class) { - attrs { - root = config - name = Name.EMPTY - this.descriptor = descriptor - this.default = default - } - } - } -} - -fun RBuilder.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null) { - div { - child(ConfigEditorComponent::class) { - attrs { - root = config - name = Name.EMPTY - this.descriptor = descriptor - this.default = default - } - } + configEditor(config, Name.EMPTY, descriptor, default) } } fun RBuilder.configEditor(obj: Configurable, descriptor: NodeDescriptor? = obj.descriptor, default: Meta? = null) { - configEditor(obj.config, descriptor ?: obj.descriptor, default) + configEditor(obj.config, Name.EMPTY, descriptor ?: obj.descriptor, default) } diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt index 64624f3c..af4faaa9 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt @@ -1,7 +1,9 @@ package hep.dataforge.vis.editor +import hep.dataforge.js.RFBuilder import hep.dataforge.js.card -import hep.dataforge.js.initState +import hep.dataforge.js.component +import hep.dataforge.js.state import hep.dataforge.names.Name import hep.dataforge.names.plus import hep.dataforge.names.startsWith @@ -26,15 +28,15 @@ interface TreeState : RState { var expanded: Boolean } -private fun RBuilder.objectTree(props: ObjectTreeProps): Unit { - var expanded: Boolean by initState{ props.selected?.startsWith(props.name) ?: false } +private fun RFBuilder.objectTree(props: ObjectTreeProps): Unit { + var expanded: Boolean by state{ props.selected?.startsWith(props.name) ?: false } val onClick: (Event) -> Unit = { expanded = !expanded } fun RBuilder.treeLabel(text: String) { - button(classes = "btn btn-link tree-label p-0") { + button(classes = "btn btn-link align-middle tree-label p-0") { +text attrs { if (props.name == props.selected) { @@ -90,7 +92,7 @@ private fun RBuilder.objectTree(props: ObjectTreeProps): Unit { } } -val ObjectTree: FunctionalComponent = functionalComponent { props -> +val ObjectTree: FunctionalComponent = component { props -> objectTree(props) } diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt new file mode 100644 index 00000000..9b71a03a --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt @@ -0,0 +1,90 @@ +package hep.dataforge.vis.editor + +import hep.dataforge.meta.Config +import hep.dataforge.meta.descriptors.ValueDescriptor +import hep.dataforge.meta.get +import hep.dataforge.meta.setValue +import hep.dataforge.meta.string +import hep.dataforge.names.Name +import hep.dataforge.values.* +import hep.dataforge.vis.widgetType +import kotlinx.html.InputType +import kotlinx.html.js.onChangeFunction +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLSelectElement +import org.w3c.dom.events.Event +import react.RBuilder +import react.dom.* + +internal fun RBuilder.valueChooser(root: Config, name: Name, value: Value, descriptor: ValueDescriptor?) { + val onValueChange: (Event) -> Unit = { + val res = when (val t = it.target) { + // (it.target as HTMLInputElement).value + is HTMLInputElement -> if (t.type == "checkbox") { + if (t.checked) True else False + } else { + t.value.asValue() + } + is HTMLSelectElement -> t.value.asValue() + else -> error("Unknown event target: $t") + } + + try { + root.setValue(name, res) + } catch (ex: Exception) { + console.error("Can't set config property ${name} to $res") + } + } + + div() { + val type = descriptor?.type?.firstOrNull() + when { + type == ValueType.BOOLEAN -> { + input(type = InputType.checkBox) { + attrs { + checked = value.boolean + onChangeFunction = onValueChange + } + } + } + type == ValueType.NUMBER -> input(type = InputType.number, classes = "form-control w-100") { + attrs { + descriptor.attributes["step"].string?.let { + step = it + } + descriptor.attributes["min"].string?.let { + min = it + } + descriptor.attributes["max"].string?.let { + max = it + } + this.defaultValue = value.string + onChangeFunction = onValueChange + } + } + descriptor?.allowedValues?.isNotEmpty() ?: false -> select (classes = "w-100") { + descriptor!!.allowedValues.forEach { + option { + +it.string + } + } + attrs { + multiple = false + onChangeFunction = onValueChange + } + } + descriptor?.widgetType == "color" -> input(type = InputType.color) { + attrs { + this.value = value.string + onChangeFunction = onValueChange + } + } + else -> input(type = InputType.text, classes = "form-control w-100") { + attrs { + this.value = value.string + onChangeFunction = onValueChange + } + } + } + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChoosers.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChoosers.kt deleted file mode 100644 index 8cfc4381..00000000 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChoosers.kt +++ /dev/null @@ -1,11 +0,0 @@ -package hep.dataforge.vis.editor - -//val TextValueChooser = functionalComponent { -// -// input(type = InputType.number, classes = "float-right") { -// attrs { -// defaultValue = value.string -// onChangeFunction = onValueChange -// } -// } -//} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/resources/css/main.css b/dataforge-vis-common/src/jsMain/resources/css/main.css index 3069e64e..d77f3a30 100644 --- a/dataforge-vis-common/src/jsMain/resources/css/main.css +++ b/dataforge-vis-common/src/jsMain/resources/css/main.css @@ -36,7 +36,7 @@ ul, .tree { } .tree-label-inactive { - color: gray; + color: lightgrey; } .tree-label-selected{ diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt index a8cb0a11..71766435 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt @@ -56,12 +56,10 @@ class Material3D : Scheme() { defineValue(OPACITY_KEY) { type(ValueType.NUMBER) default(1.0) - configure { - "attributes" to { - this["min"] = 0.0 - this["max"] = 1.0 - this["step"] = 0.1 - } + config["attributes"] = Meta { + this["min"] = 0.0 + this["max"] = 1.0 + this["step"] = 0.1 } } defineValue(WIREFRAME_KEY) { diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Proxy.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Proxy.kt index b60f8d42..557d728d 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Proxy.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Proxy.kt @@ -79,7 +79,8 @@ class Proxy private constructor( ?: error("Prototype with name $name not found in $this") } - override fun allProperties(): Laminate = Laminate(properties, mergedStyles, prototype.allProperties()) + override fun allProperties(): Laminate = + Laminate(properties, mergedStyles, prototype.allProperties(), parent?.allProperties()) override fun attachChildren() { //do nothing @@ -135,7 +136,8 @@ class Proxy private constructor( //do nothing } - override fun allProperties(): Laminate = Laminate(properties, mergedStyles, prototype.allProperties()) + override fun allProperties(): Laminate = + Laminate(properties, mergedStyles, prototype.allProperties(), parent?.allProperties()) } diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt index 463326f4..8cd0369d 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt @@ -24,6 +24,8 @@ interface VisualObject3D : VisualObject { var rotation: Point3D? var scale: Point3D? + override val descriptor: NodeDescriptor? get() = Companion.descriptor + companion object { val VISIBLE_KEY = "visible".asName() @@ -66,12 +68,13 @@ interface VisualObject3D : VisualObject { default(true) } + //TODO replace by descriptor merge + defineValue(VisualObject.STYLE_KEY){ + type(ValueType.STRING) + multiple = true + } + defineItem(Material3D.MATERIAL_KEY.toString(), Material3D.descriptor) - -// Material3D.MATERIAL_COLOR_KEY put "#ffffff" -// Material3D.MATERIAL_OPACITY_KEY put 1.0 -// Material3D.MATERIAL_WIREFRAME_KEY put false - } } } diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/MeshThreeFactory.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/MeshThreeFactory.kt index 76adef48..3cb54e1f 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/MeshThreeFactory.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/MeshThreeFactory.kt @@ -80,7 +80,7 @@ abstract class MeshThreeFactory( } fun Mesh.applyEdges(obj: VisualObject3D) { - children.find { it.name == "edges" }?.let { + children.find { it.name == "@edges" }?.let { remove(it) (it as LineSegments).dispose() } @@ -93,14 +93,14 @@ fun Mesh.applyEdges(obj: VisualObject3D) { EdgesGeometry(geometry as BufferGeometry), material ).apply { - name = "edges" + name = "@edges" } ) } } fun Mesh.applyWireFrame(obj: VisualObject3D) { - children.find { it.name == "wireframe" }?.let { + children.find { it.name == "@wireframe" }?.let { remove(it) (it as LineSegments).dispose() } @@ -112,7 +112,7 @@ fun Mesh.applyWireFrame(obj: VisualObject3D) { WireframeGeometry(geometry as BufferGeometry), material ).apply { - name = "wireframe" + name = "@wireframe" } ) } diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt index b7fcae95..0a49e96a 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt @@ -109,10 +109,10 @@ class ThreeCanvas(element: HTMLElement, val three: ThreePlugin, val canvas: Canv element.appendChild(renderer.domElement) - renderer.setSize(max(canvas.minSize, element.offsetWidth), max(canvas.minSize, element.offsetWidth)) + renderer.setSize(max(canvas.minSize, element.clientWidth), max(canvas.minSize, element.clientWidth)) - element.onresize = { - renderer.setSize(element.offsetWidth, element.offsetWidth) + window.onresize = { + renderer.setSize(element.clientWidth, element.clientWidth) camera.updateProjectionMatrix() } diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt index 9a529770..37fbc997 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt @@ -43,11 +43,6 @@ class ThreeCanvasComponent : RComponent() { canvas?.render(props.obj) } -// override fun componentWillUnmount() { -// state.element?.clear() -// props.canvasCallback?.invoke(null) -// } - override fun componentDidUpdate(prevProps: ThreeCanvasProps, prevState: ThreeCanvasState, snapshot: Any) { if (prevProps.obj != props.obj) { componentDidMount() 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 bbf1f3ee..bf05be52 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 @@ -2,13 +2,14 @@ package ru.mipt.npm.muon.monitor import hep.dataforge.context.Context import hep.dataforge.js.card +import hep.dataforge.js.component +import hep.dataforge.js.state import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.isEmpty import hep.dataforge.vis.VisualObject import hep.dataforge.vis.editor.configEditor import hep.dataforge.vis.editor.objectTree -import hep.dataforge.vis.spatial.VisualObject3D import hep.dataforge.vis.spatial.specifications.Camera import hep.dataforge.vis.spatial.specifications.Canvas import hep.dataforge.vis.spatial.three.ThreeCanvas @@ -19,7 +20,7 @@ import io.ktor.client.request.get import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.html.js.onClickFunction -import react.* +import react.RProps import react.dom.* import kotlin.math.PI @@ -27,121 +28,112 @@ interface MMAppProps : RProps { var model: Model var context: Context var connection: HttpClient -} - -interface MMAppState : RState { var selected: Name? - var canvas: ThreeCanvas? } -class MMAppComponent : RComponent() { +private val canvasConfig = Canvas { + camera = Camera { + distance = 2100.0 + latitude = PI / 6 + azimuth = PI + PI / 6 + } +} - private val onSelect: (Name?) -> Unit = { - setState { - selected = it - } +val MMApp = component { props -> + var selected by state { props.selected } + var canvas: ThreeCanvas? by state { null } + + val select: (Name?) -> Unit = { + selected = it } - private val canvasConfig = Canvas { - camera = Camera { - distance = 2100.0 - latitude = PI / 6 - azimuth = PI + PI / 6 + val visual = props.model.root + div("row") { + h1("mx-auto") { + +"Muon monitor demo" } } - - override fun RBuilder.render() { - val visual = props.model.root - val selected = state.selected - - div("row") { - div("col-lg-3") { - //tree - card("Object tree") { - objectTree(visual, selected, onSelect) + div("row") { + div("col-lg-3 mh-100 px-0") { + //tree + card("Object tree") { + objectTree(visual, selected, select) + } + } + div("col-lg-6") { + //canvas + child(ThreeCanvasComponent::class) { + attrs { + this.context = props.context + this.obj = visual + this.options = canvasConfig + this.selected = selected + this.clickCallback = select + this.canvasCallback = { + canvas = it + } } } - div("col-lg-6") { - //canvas - child(ThreeCanvasComponent::class) { - attrs { - this.context = props.context - this.obj = visual - this.options = canvasConfig - this.selected = selected - this.clickCallback = onSelect - this.canvasCallback = { - setState { - canvas = it + } + div("col-lg-3") { + div("row") { + //settings + canvas?.let { + card("Canvas configuration") { + canvasControls(it) + } + } + card("Events") { + button { + +"Next" + attrs { + onClickFunction = { + GlobalScope.launch { + val event = props.connection.get("http://localhost:8080/event") + props.model.displayEvent(event) + } + } + } + } + button { + +"Clear" + attrs { + onClickFunction = { + props.model.reset() } } } } } - div("col-lg-3") { - div("row") { - //settings - state.canvas?.let { - card("Canvas configuration") { - canvasControls(it) + div("row") { + div("container-fluid p-0") { + nav { + attrs { + attributes["aria-label"] = "breadcrumb" } - } - card("Events") { - button { - +"Next" - attrs { - onClickFunction = { - GlobalScope.launch { - val event = props.connection.get("http://localhost:8080/event") - props.model.displayEvent(event) - } - } - } - } - button { - +"Clear" - attrs { - onClickFunction = { - props.model.reset() - } - } - } - } - } - div("row") { - div("container-fluid p-0") { - nav { - attrs { - attributes["aria-label"] = "breadcrumb" - } - ol("breadcrumb") { - li("breadcrumb-item") { - button(classes = "btn btn-link p-0") { - +"World" - attrs { - onClickFunction = { - setState { - this.selected = Name.EMPTY - } - } + ol("breadcrumb") { + li("breadcrumb-item") { + button(classes = "btn btn-link p-0") { + +"World" + attrs { + onClickFunction = { + selected = hep.dataforge.names.Name.EMPTY } } } - if (selected != null) { - val tokens = ArrayList(selected.length) - selected.tokens.forEach { token -> - tokens.add(token) - val fullName = Name(tokens.toList()) - li("breadcrumb-item") { - button(classes = "btn btn-link p-0") { - +token.toString() - attrs { - onClickFunction = { - setState { - console.log("Selected = $fullName") - this.selected = fullName - } - } + } + if (selected != null) { + val tokens = ArrayList(selected?.length ?: 1) + selected?.tokens?.forEach { token -> + tokens.add(token) + val fullName = Name(tokens.toList()) + li("breadcrumb-item") { + button(classes = "btn btn-link p-0") { + +token.toString() + attrs { + onClickFunction = { + console.log("Selected = $fullName") + selected = fullName } } } @@ -151,21 +143,23 @@ class MMAppComponent : RComponent() { } } } - div("row") { - //properties - if (selected != null) { + } + div("row") { + //properties + card("Properties") { + selected.let { selected -> val selectedObject: VisualObject? = when { + selected == null -> null selected.isEmpty() -> visual else -> visual[selected] } if (selectedObject != null) { - card("Properties") { - configEditor(selectedObject, descriptor = VisualObject3D.descriptor) - } + configEditor(selectedObject, default = selectedObject.allProperties()) } } } } } + } } \ No newline at end of file diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt index 8346df88..86482004 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt @@ -8,6 +8,7 @@ import io.ktor.client.HttpClient import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.serializer.KotlinxSerializer import kotlinx.serialization.json.Json +import react.child import react.dom.render import kotlin.browser.document @@ -29,7 +30,7 @@ private class MMDemoApp : Application { val element = document.getElementById("app") ?: error("Element with id 'app' not found on page") render(element) { - child(MMAppComponent::class) { + child(MMApp) { attrs { model = this@MMDemoApp.model connection = this@MMDemoApp.connection diff --git a/demo/muon-monitor/src/jsMain/resources/index.html b/demo/muon-monitor/src/jsMain/resources/index.html index 9d5118c6..a229b2a9 100644 --- a/demo/muon-monitor/src/jsMain/resources/index.html +++ b/demo/muon-monitor/src/jsMain/resources/index.html @@ -11,25 +11,6 @@ - -

-

Muon monitor demo

-
- -
-
- -
+
\ No newline at end of file