diff --git a/demo/js-playground/src/main/kotlin/JsPlaygroundApp.kt b/demo/js-playground/src/main/kotlin/JsPlaygroundApp.kt index 509eb0ab..74ff9fd9 100644 --- a/demo/js-playground/src/main/kotlin/JsPlaygroundApp.kt +++ b/demo/js-playground/src/main/kotlin/JsPlaygroundApp.kt @@ -1,4 +1,8 @@ import kotlinx.browser.document +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.css.* import react.child import react.dom.render @@ -17,6 +21,7 @@ import space.kscience.visionforge.solid.* import space.kscience.visionforge.startApplication import styled.css import styled.styledDiv +import kotlin.math.sqrt import kotlin.random.Random private class JsPlaygroundApp : Application { @@ -31,9 +36,38 @@ private class JsPlaygroundApp : Application { val element = document.getElementById("playground") ?: error("Element with id 'playground' not found on page") + val bouncingSphere = SolidGroup { + sphere(5.0, "ball") { + detail = 16 + color("red") + val h = 100.0 + y = h + GlobalScope.launch { + val g = 10.0 + val dt = 0.1 + var time = 0.0 + var velocity = 0.0 + while (isActive) { + delay(20) + time += dt + velocity -= g * dt + y = y.toDouble() + velocity * dt + if (y.toDouble() <= 2.5){ + velocity = sqrt(2*g*h) + } + } + } + } + + box(200, 5, 200, name = "floor"){ + y = -2.5 + } + } + val visionOfD0 = GdmlShowCase.babyIaxo().toVision() val random = Random(112233) + val visionOfSpheres = SolidGroup { repeat(100) { sphere(5, name = "sphere[$it]") { @@ -56,7 +90,16 @@ private class JsPlaygroundApp : Application { height = 100.vh width = 100.vw } - SmartTabs("D0") { + SmartTabs("gravity") { + Tab("gravity") { + child(ThreeCanvasWithControls) { + attrs { + context = playgroundContext + solid = bouncingSphere + } + } + } + Tab("D0") { child(ThreeCanvasWithControls) { attrs { @@ -73,8 +116,8 @@ private class JsPlaygroundApp : Application { } } } - Tab("plotly"){ - Plotly{ + Tab("plotly") { + Plotly { attrs { context = playgroundContext plot = space.kscience.plotly.Plotly.plot { diff --git a/ui/bootstrap/src/main/kotlin/space/kscience/visionforge/bootstrap/outputConfig.kt b/ui/bootstrap/src/main/kotlin/space/kscience/visionforge/bootstrap/outputConfig.kt index 8088d2fd..7c7e7b37 100644 --- a/ui/bootstrap/src/main/kotlin/space/kscience/visionforge/bootstrap/outputConfig.kt +++ b/ui/bootstrap/src/main/kotlin/space/kscience/visionforge/bootstrap/outputConfig.kt @@ -66,7 +66,7 @@ public val CanvasControls: FunctionComponent = functionalCo } } propertyEditor( - ownProperties = props.canvasOptions, + ownProperties = props.canvasOptions.meta, allProperties = props.canvasOptions.meta.withDefault(Canvas3DOptions.descriptor.defaultNode), descriptor = Canvas3DOptions.descriptor, expanded = false diff --git a/ui/bootstrap/src/main/kotlin/space/kscience/visionforge/bootstrap/visionPropertyEditor.kt b/ui/bootstrap/src/main/kotlin/space/kscience/visionforge/bootstrap/visionPropertyEditor.kt index 65255af6..a65c1f42 100644 --- a/ui/bootstrap/src/main/kotlin/space/kscience/visionforge/bootstrap/visionPropertyEditor.kt +++ b/ui/bootstrap/src/main/kotlin/space/kscience/visionforge/bootstrap/visionPropertyEditor.kt @@ -4,10 +4,13 @@ import org.w3c.dom.Element import react.RBuilder import react.dom.render import space.kscience.dataforge.meta.descriptors.MetaDescriptor -import space.kscience.visionforge.* +import space.kscience.visionforge.Vision +import space.kscience.visionforge.computeProperties +import space.kscience.visionforge.getStyle import space.kscience.visionforge.react.metaViewer import space.kscience.visionforge.react.propertyEditor import space.kscience.visionforge.solid.SolidReference +import space.kscience.visionforge.styles public fun RBuilder.visionPropertyEditor( vision: Vision, @@ -19,7 +22,6 @@ public fun RBuilder.visionPropertyEditor( propertyEditor( ownProperties = vision.meta, allProperties = vision.computeProperties(), - updateFlow = vision.propertyChanges, descriptor = descriptor, key = key ) diff --git a/ui/react/src/main/kotlin/space/kscience/visionforge/react/MultiSelectChooser.kt b/ui/react/src/main/kotlin/space/kscience/visionforge/react/MultiSelectChooser.kt index 9f698df7..84181340 100644 --- a/ui/react/src/main/kotlin/space/kscience/visionforge/react/MultiSelectChooser.kt +++ b/ui/react/src/main/kotlin/space/kscience/visionforge/react/MultiSelectChooser.kt @@ -10,7 +10,6 @@ import react.dom.attrs import react.dom.option import react.dom.select import react.functionalComponent -import react.useState import space.kscience.dataforge.meta.descriptors.allowedValues import space.kscience.dataforge.values.asValue import space.kscience.dataforge.values.string @@ -18,19 +17,16 @@ import space.kscience.dataforge.values.string @JsExport public val MultiSelectChooser: FunctionComponent = functionalComponent("MultiSelectChooser") { props -> - var selectedItems by useState { props.item?.value?.list ?: emptyList() } - val onChange: (Event) -> Unit = { event: Event -> val newSelected = (event.target as HTMLSelectElement).selectedOptions.asList() .map { (it as HTMLOptionElement).value.asValue() } - props.valueChanged?.invoke(newSelected.asValue()) - selectedItems = newSelected + props.meta.value = newSelected.asValue() } select { attrs { multiple = true - values = selectedItems.mapTo(HashSet()) { it.string } + values = (props.actual.value?.list ?: emptyList()).mapTo(HashSet()) { it.string } onChangeFunction = onChange } props.descriptor?.allowedValues?.forEach { optionValue -> diff --git a/ui/react/src/main/kotlin/space/kscience/visionforge/react/PropertyEditor.kt b/ui/react/src/main/kotlin/space/kscience/visionforge/react/PropertyEditor.kt index 47b0d818..9ff77f2c 100644 --- a/ui/react/src/main/kotlin/space/kscience/visionforge/react/PropertyEditor.kt +++ b/ui/react/src/main/kotlin/space/kscience/visionforge/react/PropertyEditor.kt @@ -1,14 +1,5 @@ package space.kscience.visionforge.react -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import kotlinx.css.* import kotlinx.css.properties.TextDecoration import kotlinx.html.js.onClickFunction @@ -22,7 +13,6 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.ValueRequirement import space.kscience.dataforge.meta.descriptors.get import space.kscience.dataforge.names.* -import space.kscience.dataforge.values.Value import space.kscience.visionforge.hidden import styled.css import styled.styledButton @@ -32,17 +22,17 @@ import styled.styledSpan public external interface PropertyEditorProps : RProps { /** - * Root config object - always non null + * Root config object - always non-null */ - public var ownProperties: MutableMetaProvider + public var meta: ObservableMutableMeta /** * Provide default item (greyed out if used) */ - public var allProperties: MetaProvider? + public var withDefault: MetaProvider /** - * Full path to the displayed node in [ownProperties]. Could be empty + * Full path to the displayed node in [meta]. Could be empty */ public var name: Name @@ -51,16 +41,6 @@ public external interface PropertyEditorProps : RProps { */ public var descriptor: MetaDescriptor? - /** - * A coroutine scope for updates - */ - public var scope: CoroutineScope? - - /** - * Flow names of updated properties - */ - public var updateFlow: Flow? - /** * Initial expanded state */ @@ -68,67 +48,59 @@ public external interface PropertyEditorProps : RProps { } private val PropertyEditorItem: FunctionComponent = - functionalComponent("ConfigEditorItem") { props -> + functionalComponent("PropertyEditorItem") { props -> propertyEditorItem(props) } private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) { var expanded: Boolean by useState { props.expanded ?: true } - val descriptor: MetaDescriptor? = props.descriptor?.get(props.name) - var ownProperty: Meta? by useState { props.ownProperties.getMeta(props.name) } - val actualMeta = props.allProperties?.getMeta(props.name) + val descriptor: MetaDescriptor? = useMemo(props.descriptor, props.name) { props.descriptor?.get(props.name) } + var ownProperty: ObservableMutableMeta by useState { props.meta.getOrCreate(props.name) } + + val keys = useMemo(descriptor) { + buildSet { + descriptor?.children?.filterNot { + it.key.startsWith("@") || it.value.hidden + }?.forEach { + add(NameToken(it.key)) + } + //ownProperty?.items?.keys?.filterNot { it.body.startsWith("@") }?.let { addAll(it) } + } + } val token = props.name.lastOrNull()?.toString() ?: "Properties" fun update() { - ownProperty = props.ownProperties.getMeta(props.name) + ownProperty = props.meta.getOrCreate(props.name) } - if (props.updateFlow != null) { - useEffect(props.ownProperties, props.updateFlow) { - val updateJob = props.updateFlow!!.onEach { updatedName -> - if (updatedName == props.name) { - update() - } - }.launchIn(props.scope ?: GlobalScope) - cleanup { - updateJob.cancel() + useEffect(props.meta) { + props.meta.onChange(props) { updatedName -> + if (updatedName == props.name) { + update() } } + cleanup { + props.meta.removeListener(props) + } } val expanderClick: (Event) -> Unit = { expanded = !expanded } - val valueChanged: (Value?) -> Unit = { - if (it == null) { - props.ownProperties.remove(props.name) - } else { - props.ownProperties.setValue(props.name, it) - } - update() - } - val removeClick: (Event) -> Unit = { - props.ownProperties.remove(props.name) + props.meta.remove(props.name) update() } - val keys = buildSet { - descriptor?.children?.filterNot { - it.key.startsWith("@") || it.value.hidden - }?.forEach { - add(NameToken(it.key)) - } - //ownProperty?.items?.keys?.filterNot { it.body.startsWith("@") }?.let { addAll(it) } - } + flexRow { css { alignItems = Align.center } - if(keys.isNotEmpty()) { + if (keys.isNotEmpty()) { styledSpan { css { +TreeStyles.treeCaret @@ -144,25 +116,26 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) { styledSpan { css { +TreeStyles.treeLabel - if (ownProperty == null) { + if (ownProperty.isEmpty()) { +TreeStyles.treeLabelInactive } } +token } - if(!props.name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) { + if (!props.name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) { styledDiv { css { //+TreeStyles.resizeableInput width = 160.px margin(1.px, 5.px) } - valueChooser( - props.name, - actualMeta, - descriptor, - valueChanged - ) + ValueChooser{ + attrs { + this.descriptor = descriptor + this.meta = ownProperty + this.actual = props.withDefault.getMeta(props.name) ?: ownProperty + } + } } styledButton { @@ -184,7 +157,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) { } +"\u00D7" attrs { - if (ownProperty == null) { + if (ownProperty.isEmpty()) { disabled = true } else { onClickFunction = removeClick @@ -206,8 +179,8 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) { child(PropertyEditorItem) { attrs { this.key = props.name.toString() - this.ownProperties = props.ownProperties - this.allProperties = props.allProperties + this.meta = props.meta + this.withDefault = props.withDefault this.name = props.name + token this.descriptor = props.descriptor } @@ -217,74 +190,52 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) { } } } - } - @JsExport public val PropertyEditor: FunctionComponent = functionalComponent("PropertyEditor") { props -> child(PropertyEditorItem) { attrs { this.key = "" - this.ownProperties = props.ownProperties - this.allProperties = props.allProperties + this.meta = props.meta + this.withDefault = props.withDefault this.name = Name.EMPTY this.descriptor = props.descriptor - this.scope = props.scope this.expanded = props.expanded } } } public fun RBuilder.propertyEditor( - ownProperties: MutableMetaProvider, - allProperties: MetaProvider? = ownProperties, - updateFlow: Flow? = null, + ownProperties: ObservableMutableMeta, + allProperties: MetaProvider = ownProperties, descriptor: MetaDescriptor? = null, - scope: CoroutineScope? = null, key: Any? = null, expanded: Boolean? = null ) { child(PropertyEditor) { attrs { - this.ownProperties = ownProperties - this.allProperties = allProperties - this.updateFlow = updateFlow + this.meta = ownProperties + this.withDefault = allProperties this.descriptor = descriptor this.key = key?.toString() ?: "" - this.scope = scope this.expanded = expanded } } } -@OptIn(ExperimentalCoroutinesApi::class) -private fun ObservableMutableMeta.flowUpdates(): Flow = callbackFlow { - onChange(this) { name -> - launch { - send(name) - } - } - awaitClose { - removeListener(this) - } -} - - public fun RBuilder.configEditor( config: ObservableMutableMeta, - default: MetaProvider? = null, + default: MetaProvider = config, descriptor: MetaDescriptor? = null, key: Any? = null, - scope: CoroutineScope? = null, -): Unit = propertyEditor(config, default, config.flowUpdates(), descriptor, scope, key = key) +): Unit = propertyEditor(config, default, descriptor, key = key) public fun Element.configEditor( config: ObservableMutableMeta, + default: Meta = config, descriptor: MetaDescriptor? = null, - default: Meta? = null, key: Any? = null, - scope: CoroutineScope? = null, ): Unit = render(this) { - configEditor(config, default, descriptor, key, scope) + configEditor(config, default, descriptor, key = key) } \ No newline at end of file diff --git a/ui/react/src/main/kotlin/space/kscience/visionforge/react/RangeValueChooser.kt b/ui/react/src/main/kotlin/space/kscience/visionforge/react/RangeValueChooser.kt index 28513416..c753271f 100644 --- a/ui/react/src/main/kotlin/space/kscience/visionforge/react/RangeValueChooser.kt +++ b/ui/react/src/main/kotlin/space/kscience/visionforge/react/RangeValueChooser.kt @@ -10,6 +10,7 @@ import react.FunctionComponent import react.dom.attrs import react.functionalComponent import react.useState +import space.kscience.dataforge.meta.descriptors.ValueRequirement import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.string @@ -20,30 +21,32 @@ import styled.styledInput @JsExport public val RangeValueChooser: FunctionComponent = functionalComponent("RangeValueChooser") { props -> - var innerValue by useState(props.item.double) - var rangeDisabled: Boolean by useState(props.item == null) + var innerValue by useState(props.actual.double) + var rangeDisabled: Boolean by useState(props.meta.value == null) val handleDisable: (Event) -> Unit = { val checkBoxValue = (it.target as HTMLInputElement).checked rangeDisabled = !checkBoxValue - if(!checkBoxValue) { - props.valueChanged?.invoke(null) + props.meta.value = if(!checkBoxValue) { + null } else { - props.valueChanged?.invoke(innerValue?.asValue()) + innerValue?.asValue() } } val handleChange: (Event) -> Unit = { val newValue = (it.target as HTMLInputElement).value - props.valueChanged?.invoke(newValue.toDoubleOrNull()?.asValue()) + props.meta.value = newValue.toDoubleOrNull()?.asValue() innerValue = newValue.toDoubleOrNull() } flexRow { - styledInput(type = InputType.checkBox) { - attrs { - defaultChecked = rangeDisabled.not() - onChangeFunction = handleDisable + if(props.descriptor?.valueRequirement != ValueRequirement.REQUIRED) { + styledInput(type = InputType.checkBox) { + attrs { + defaultChecked = rangeDisabled.not() + onChangeFunction = handleDisable + } } } @@ -55,6 +58,7 @@ public val RangeValueChooser: FunctionComponent = disabled = rangeDisabled value = innerValue?.toString() ?: "" onChangeFunction = handleChange + consumer.onTagEvent(this, "input", handleChange) val minValue = props.descriptor?.attributes?.get("min").string minValue?.let { min = it diff --git a/ui/react/src/main/kotlin/space/kscience/visionforge/react/valueChooser.kt b/ui/react/src/main/kotlin/space/kscience/visionforge/react/valueChooser.kt index 2cfcc976..e29f0f8b 100644 --- a/ui/react/src/main/kotlin/space/kscience/visionforge/react/valueChooser.kt +++ b/ui/react/src/main/kotlin/space/kscience/visionforge/react/valueChooser.kt @@ -13,14 +13,13 @@ import org.w3c.dom.events.Event import react.* import react.dom.attrs import react.dom.option -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.boolean +import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.allowedValues -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.meta.string -import space.kscience.dataforge.names.Name -import space.kscience.dataforge.values.* +import space.kscience.dataforge.values.ValueType +import space.kscience.dataforge.values.asValue +import space.kscience.dataforge.values.int +import space.kscience.dataforge.values.string import space.kscience.visionforge.Colors import space.kscience.visionforge.widgetType import styled.css @@ -28,23 +27,19 @@ import styled.styledInput import styled.styledSelect public external interface ValueChooserProps : RProps { - public var item: Meta? public var descriptor: MetaDescriptor? - - //public var nullable: Boolean? - public var valueChanged: ((Value?) -> Unit)? + public var meta: ObservableMutableMeta + public var actual: Meta } @JsExport public val StringValueChooser: FunctionComponent = functionalComponent("StringValueChooser") { props -> - var value by useState(props.item.string ?: "") + var value by useState(props.actual.string ?: "") val keyDown: (Event) -> Unit = { event -> if (event.type == "keydown" && event.asDynamic().key == "Enter") { value = (event.target as HTMLInputElement).value - if (value != props.item.string) { - props.valueChanged?.invoke(value.asValue()) - } + props.meta.value = value.asValue() } } val handleChange: (Event) -> Unit = { @@ -67,7 +62,7 @@ public val BooleanValueChooser: FunctionComponent = functionalComponent("BooleanValueChooser") { props -> val handleChange: (Event) -> Unit = { val newValue = (it.target as HTMLInputElement).checked - props.valueChanged?.invoke(newValue.asValue()) + props.meta.value = newValue.asValue() } styledInput(type = InputType.checkBox) { css { @@ -75,7 +70,7 @@ public val BooleanValueChooser: FunctionComponent = } attrs { //this.attributes["indeterminate"] = (props.item == null).toString() - defaultChecked = props.item.boolean ?: false + defaultChecked = props.actual.boolean ?: false onChangeFunction = handleChange } } @@ -84,7 +79,7 @@ public val BooleanValueChooser: FunctionComponent = @JsExport public val NumberValueChooser: FunctionComponent = functionalComponent("NumberValueChooser") { props -> - var innerValue by useState(props.item.string ?: "") + var innerValue by useState(props.actual.string ?: "") val keyDown: (Event) -> Unit = { event -> if (event.type == "keydown" && event.asDynamic().key == "Enter") { innerValue = (event.target as HTMLInputElement).value @@ -92,7 +87,7 @@ public val NumberValueChooser: FunctionComponent = if (number == null) { console.error("The input value $innerValue is not a number") } else { - props.valueChanged?.invoke(number.asValue()) + props.meta.value = number.asValue() } } } @@ -123,10 +118,10 @@ public val NumberValueChooser: FunctionComponent = @JsExport public val ComboValueChooser: FunctionComponent = functionalComponent("ComboValueChooser") { props -> - var selected by useState(props.item.string ?: "") + var selected by useState(props.actual.string ?: "") val handleChange: (Event) -> Unit = { selected = (it.target as HTMLSelectElement).value - props.valueChanged?.invoke(selected.asValue()) + props.meta.value = selected.asValue() } styledSelect { css { @@ -138,7 +133,7 @@ public val ComboValueChooser: FunctionComponent = } } attrs { - this.value = props.item?.string ?: "" + this.value = props.actual.string ?: "" multiple = false onChangeFunction = handleChange } @@ -149,14 +144,14 @@ public val ComboValueChooser: FunctionComponent = public val ColorValueChooser: FunctionComponent = functionalComponent("ColorValueChooser") { props -> var value by useState( - props.item?.value?.let { value -> + props.actual.value?.let { value -> if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int) else value.string } ?: "#000000" ) val handleChange: (Event) -> Unit = { value = (it.target as HTMLInputElement).value - props.valueChanged?.invoke(value.asValue()) + props.meta.value = value.asValue() } styledInput(type = InputType.color) { css { @@ -189,19 +184,3 @@ public val ValueChooser: FunctionComponent = functionalCompon else -> child(StringValueChooser, props) } } - -internal fun RBuilder.valueChooser( - name: Name, - item: Meta?, - descriptor: MetaDescriptor? = null, - callback: (Value?) -> Unit, -) { - child(ValueChooser) { - attrs { - key = name.toString() - this.item = item - this.descriptor = descriptor - this.valueChanged = callback - } - } -} diff --git a/ui/ring/src/main/kotlin/space.kscience.visionforge.ring/ThreeViewWithControls.kt b/ui/ring/src/main/kotlin/space.kscience.visionforge.ring/ThreeViewWithControls.kt index f8439a07..a14d23ca 100644 --- a/ui/ring/src/main/kotlin/space.kscience.visionforge.ring/ThreeViewWithControls.kt +++ b/ui/ring/src/main/kotlin/space.kscience.visionforge.ring/ThreeViewWithControls.kt @@ -15,7 +15,6 @@ import space.kscience.dataforge.names.isEmpty import space.kscience.dataforge.names.length import space.kscience.visionforge.VisionGroup import space.kscience.visionforge.computeProperties -import space.kscience.visionforge.propertyChanges import space.kscience.visionforge.react.ThreeCanvasComponent import space.kscience.visionforge.react.flexColumn import space.kscience.visionforge.react.flexRow @@ -137,7 +136,6 @@ public val ThreeCanvasWithControls: FunctionComponent = functional } } propertyEditor( - ownProperties = props.options, + ownProperties = props.options.meta, allProperties = props.options.meta.withDefault(Canvas3DOptions.descriptor.defaultNode), descriptor = Canvas3DOptions.descriptor, expanded = false diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt index e7bdd301..924dfdd1 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -12,10 +12,12 @@ import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.Type import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName +import space.kscience.dataforge.names.startsWith import space.kscience.dataforge.values.Value import space.kscience.dataforge.values.asValue import space.kscience.dataforge.values.boolean import space.kscience.visionforge.Vision.Companion.TYPE +import kotlin.reflect.KProperty1 /** * A root type for display hierarchy @@ -129,3 +131,17 @@ public var Vision.visible: Boolean? get() = getPropertyValue(Vision.VISIBLE_KEY)?.boolean set(value) = meta.setValue(Vision.VISIBLE_KEY, value?.asValue()) + +public fun V.useProperty( + property: KProperty1, + owner: Any? = null, + callBack: V.(T) -> Unit, +) { + //Pass initial value. + callBack(property.get(this)) + meta.onChange(owner) { name -> + if (name.startsWith(property.name.asName())) { + callBack(property.get(this@useProperty)) + } + } +} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionBase.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionBase.kt index c7d9413c..74f173ae 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionBase.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionBase.kt @@ -44,7 +44,7 @@ public open class VisionBase( } @Transient - private val listeners = HashSet() + private val listeners: MutableList = mutableListOf() private inner class VisionProperties(val pathName: Name) : ObservableMutableMeta { diff --git a/visionforge-markdown/src/commonMain/kotlin/space/kscience/visionforge/markup/VisionOfMarkup.kt b/visionforge-markdown/src/commonMain/kotlin/space/kscience/visionforge/markup/VisionOfMarkup.kt index 89a6836e..58708ab1 100644 --- a/visionforge-markdown/src/commonMain/kotlin/space/kscience/visionforge/markup/VisionOfMarkup.kt +++ b/visionforge-markdown/src/commonMain/kotlin/space/kscience/visionforge/markup/VisionOfMarkup.kt @@ -11,7 +11,6 @@ import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionBase -import space.kscience.visionforge.setProperty @Serializable @SerialName("vision.markup") @@ -20,15 +19,11 @@ public class VisionOfMarkup( ) : VisionBase() { //FIXME to be removed after https://github.com/Kotlin/kotlinx.serialization/issues/1602 fix - override var properties: MutableMeta? = null + protected override var properties: MutableMeta? = null //TODO add templates - public var content: String? - get() = meta.getMeta(CONTENT_PROPERTY_KEY).string - set(value) { - setProperty(CONTENT_PROPERTY_KEY, value) - } + public var content: String? by meta.string(CONTENT_PROPERTY_KEY) public companion object { public val CONTENT_PROPERTY_KEY: Name = "content".asName() diff --git a/visionforge-markdown/src/jsMain/kotlin/space/kscience/visionforge/markup/MarkupPlugin.kt b/visionforge-markdown/src/jsMain/kotlin/space/kscience/visionforge/markup/MarkupPlugin.kt index 30b8b848..d26488c5 100644 --- a/visionforge-markdown/src/jsMain/kotlin/space/kscience/visionforge/markup/MarkupPlugin.kt +++ b/visionforge-markdown/src/jsMain/kotlin/space/kscience/visionforge/markup/MarkupPlugin.kt @@ -1,16 +1,19 @@ package space.kscience.visionforge.markup import kotlinx.browser.document +import kotlinx.dom.clear +import kotlinx.html.dom.append import kotlinx.serialization.modules.SerializersModule +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor import org.w3c.dom.Element import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginTag import space.kscience.dataforge.meta.Meta -import space.kscience.visionforge.ElementVisionRenderer -import space.kscience.visionforge.Vision -import space.kscience.visionforge.VisionClient -import space.kscience.visionforge.VisionPlugin +import space.kscience.visionforge.* +import space.kscience.visionforge.markup.VisionOfMarkup.Companion.COMMONMARK_FORMAT +import space.kscience.visionforge.markup.VisionOfMarkup.Companion.GFM_FORMAT import kotlin.reflect.KClass public class MarkupPlugin : VisionPlugin(), ElementVisionRenderer { @@ -26,8 +29,20 @@ public class MarkupPlugin : VisionPlugin(), ElementVisionRenderer { override fun render(element: Element, vision: Vision, meta: Meta) { require(vision is VisionOfMarkup) { "The vision is not a markup vision" } val div = document.createElement("div") + val flavour = when (vision.format) { + COMMONMARK_FORMAT -> CommonMarkFlavourDescriptor() + GFM_FORMAT -> GFMFlavourDescriptor() + //TODO add new formats via plugins + else-> error("Format ${vision.format} not recognized") + } + vision.useProperty(VisionOfMarkup::content) { + div.clear() + div.append { + markdown(flavour) { vision.content ?: "" } + + } + } element.append(div) - TODO() } public companion object : PluginFactory { diff --git a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/specifications/Canvas3DOptions.kt b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/specifications/Canvas3DOptions.kt index 01f6e74e..4a819ff7 100644 --- a/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/specifications/Canvas3DOptions.kt +++ b/visionforge-solid/src/commonMain/kotlin/space/kscience/visionforge/solid/specifications/Canvas3DOptions.kt @@ -5,7 +5,6 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.scheme import space.kscience.dataforge.meta.descriptors.value import space.kscience.dataforge.names.Name -import space.kscience.dataforge.values.ValueType import space.kscience.visionforge.hide import space.kscience.visionforge.widgetType @@ -21,20 +20,21 @@ public class Clipping : Scheme() { attributes["min"] = 0.0 attributes["max"] = 1.0 attributes["step"] = 0.01 + default(1.0) } value(Clipping::y) { widgetType = "range" attributes["min"] = 0.0 attributes["max"] = 1.0 attributes["step"] = 0.01 - + default(1.0) } value(Clipping::z) { widgetType = "range" attributes["min"] = 0.0 attributes["max"] = 1.0 attributes["step"] = 0.01 - + default(1.0) } } } diff --git a/visionforge-threejs/src/main/kotlin/space/kscience/visionforge/solid/three/ThreeCanvas.kt b/visionforge-threejs/src/main/kotlin/space/kscience/visionforge/solid/three/ThreeCanvas.kt index 2cfcadf5..0d223987 100644 --- a/visionforge-threejs/src/main/kotlin/space/kscience/visionforge/solid/three/ThreeCanvas.kt +++ b/visionforge-threejs/src/main/kotlin/space/kscience/visionforge/solid/three/ThreeCanvas.kt @@ -168,42 +168,40 @@ public class ThreeCanvas( } //Clipping planes - options.meta.onChange(this@ThreeCanvas) { name-> - if (name.startsWith(Canvas3DOptions::clipping.name.asName())) { - val clipping = options.clipping - if (!clipping.meta.isEmpty()) { - renderer.localClippingEnabled = true - boundingBox?.let { boundingBox -> - val xClippingPlane = clipping.x?.let { - val absoluteValue = boundingBox.min.x + (boundingBox.max.x - boundingBox.min.x) * it - Plane(Vector3(-1.0, 0.0, 0.0), absoluteValue) - - } - val yClippingPlane = clipping.y?.let { - val absoluteValue = boundingBox.min.y + (boundingBox.max.y - boundingBox.min.y) * it - Plane(Vector3(0.0, -1.0, 0.0), absoluteValue) - } - - val zClippingPlane = clipping.z?.let { - val absoluteValue = boundingBox.min.z + (boundingBox.max.z - boundingBox.min.z) * it - Plane(Vector3(0.0, 0.0, -1.0), absoluteValue) - } - renderer.clippingPlanes = - listOfNotNull(xClippingPlane, yClippingPlane, zClippingPlane).toTypedArray() + options.useProperty(Canvas3DOptions::clipping){clipping -> + if (!clipping.meta.isEmpty()) { + renderer.localClippingEnabled = true + boundingBox?.let { boundingBox -> + val xClippingPlane = clipping.x?.let { + val absoluteValue = boundingBox.min.x + (boundingBox.max.x - boundingBox.min.x) * it + Plane(Vector3(-1.0, 0.0, 0.0), absoluteValue) + } + val yClippingPlane = clipping.y?.let { + val absoluteValue = boundingBox.min.y + (boundingBox.max.y - boundingBox.min.y) * it + Plane(Vector3(0.0, -1.0, 0.0), absoluteValue) } - } else { - renderer.localClippingEnabled = false - } - } else if (name.startsWith(Canvas3DOptions::size.name.asName())) { - canvas.style.apply { - minWidth = "${options.size.minWith.toInt()}px" - maxWidth = "${options.size.maxWith.toInt()}px" - minHeight = "${options.size.minHeight.toInt()}px" - maxHeight = "${options.size.maxHeight.toInt()}px" - } - } + val zClippingPlane = clipping.z?.let { + val absoluteValue = boundingBox.min.z + (boundingBox.max.z - boundingBox.min.z) * it + Plane(Vector3(0.0, 0.0, -1.0), absoluteValue) + } + renderer.clippingPlanes = + listOfNotNull(xClippingPlane, yClippingPlane, zClippingPlane).toTypedArray() + } + } else { + renderer.localClippingEnabled = false + } } + + options.useProperty(Canvas3DOptions::size){ + canvas.style.apply { + minWidth = "${options.size.minWith.toInt()}px" + maxWidth = "${options.size.maxWith.toInt()}px" + minHeight = "${options.size.minHeight.toInt()}px" + maxHeight = "${options.size.maxHeight.toInt()}px" + } + } + } /**