diff --git a/settings.gradle.kts b/settings.gradle.kts index 31119a06..8611d0aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,7 +45,7 @@ include( ":ui:ring", // ":ui:material", ":ui:bootstrap", -// ":ui:compose", + ":ui:compose", ":visionforge-core", ":visionforge-solid", // ":visionforge-fx", diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt index 93cbddfc..5aaf4140 100644 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/MetaViewer.kt @@ -1,11 +1,12 @@ package space.kscience.visionforge.compose import androidx.compose.runtime.* -import kotlinx.html.js.onClickFunction import org.jetbrains.compose.web.css.AlignItems import org.jetbrains.compose.web.css.alignItems +import org.jetbrains.compose.web.dom.A +import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Span -import org.w3c.dom.events.Event +import org.jetbrains.compose.web.dom.Text import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.get @@ -17,10 +18,6 @@ import space.kscience.dataforge.names.lastOrNull import space.kscience.dataforge.names.plus -private val MetaViewerItem: FC = fc("MetaViewerItem") { props -> - metaViewerItem(props) -} - @Composable private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescriptor? = null) { var expanded: Boolean by remember { mutableStateOf(true) } @@ -29,11 +26,7 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto val actualValue = item?.value ?: descriptorItem?.defaultValue val actualMeta = item ?: descriptorItem?.defaultNode - val token = name.lastOrNull()?.toString() ?: props.rootName ?: "" - - val expanderClick: (Event) -> Unit = { - expanded = !expanded - } + val token = name.lastOrNull()?.toString() ?: "" FlexRow(attrs = { classes("metaItem") @@ -42,42 +35,34 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto } }) { if (actualMeta?.isLeaf == false) { - Span(attrs = { - + Span({ + classes(TreeStyles.treeCaret) + if (expanded) { + classes(TreeStyles.treeCaretDown) + } + onClick { expanded = !expanded } }) - styledSpan { - css { - +TreeStyles.treeCaret - if (expanded) { - +TreeStyles.treeCaredDown - } - } - attrs { - onClickFunction = expanderClick - } - } } - styledSpan { - css { - +TreeStyles.treeLabel - if (item == null) { - +TreeStyles.treeLabelInactive - } + Span({ + classes(TreeStyles.treeLabel) + if (item == null) { + classes(TreeStyles.treeLabelInactive) } - +token + }) { + Text(token) } - styledDiv { - a { - +actualValue.toString() + + Div { + A { + Text(actualValue.toString()) } } } if (expanded) { - flexColumn { - css { - +TreeStyles.tree - } + FlexColumn({ + classes(TreeStyles.tree) + }) { val keys = buildSet { descriptorItem?.children?.keys?.forEach { add(NameToken(it)) @@ -86,45 +71,17 @@ private fun MetaViewerItem(root: Meta, name: Name, rootDescriptor: MetaDescripto } keys.filter { !it.body.startsWith("@") }.forEach { token -> - styledDiv { - css { - +TreeStyles.treeItem - } - child(MetaViewerItem) { - attrs { - this.key = props.name.toString() - this.root = props.root - this.name = props.name + token - this.descriptor = props.descriptor - } - } - //configEditor(props.root, props.name + token, props.descriptor, props.default) + Div({ + classes(TreeStyles.treeItem) + }) { + MetaViewerItem(root, name + token, rootDescriptor) } } } } - - } -@JsExport -public val MetaViewer: FC = fc("MetaViewer") { props -> - child(MetaViewerItem) { - attrs { - this.key = "" - this.root = props.root - this.name = Name.EMPTY - this.descriptor = props.descriptor - } - } -} - -public fun RBuilder.metaViewer(meta: Meta, descriptor: MetaDescriptor? = null, key: Any? = null) { - child(MetaViewer) { - attrs { - this.key = key?.toString() ?: "" - this.root = meta - this.descriptor = descriptor - } - } +@Composable +public fun MetaViewer(meta: Meta, descriptor: MetaDescriptor? = null) { + MetaViewerItem(meta, Name.EMPTY, descriptor) } diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt new file mode 100644 index 00000000..f82b9e05 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/PropertyEditor.kt @@ -0,0 +1,189 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import org.jetbrains.compose.web.attributes.disabled +import org.jetbrains.compose.web.css.AlignItems +import org.jetbrains.compose.web.css.alignItems +import org.jetbrains.compose.web.css.px +import org.jetbrains.compose.web.css.width +import org.jetbrains.compose.web.dom.Button +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text +import space.kscience.dataforge.meta.MutableMeta +import space.kscience.dataforge.meta.ObservableMutableMeta +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.meta.get +import space.kscience.dataforge.meta.remove +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.NameToken +import space.kscience.dataforge.names.isEmpty +import space.kscience.dataforge.names.lastOrNull +import space.kscience.visionforge.hidden + + +/** + * The display state of a property + */ +public sealed class EditorPropertyState { + public object Defined : EditorPropertyState() + public class Default(public val source: String = "unknown") : EditorPropertyState() + public object Undefined : EditorPropertyState() + +} + +/** + * @param rootDescriptor Full path to the displayed node in [meta]. Could be empty + */ +@Composable +private fun PropertyEditorItem( + /** + * Root config object - always non-null + */ + meta: MutableMeta, + getPropertyState: (Name) -> EditorPropertyState, + scope: CoroutineScope, + updates: Flow, + name: Name, + rootDescriptor: MetaDescriptor?, + initialExpanded: Boolean? = null, +) { + var expanded: Boolean by remember { mutableStateOf(initialExpanded ?: true) } + val descriptor: MetaDescriptor? = remember(rootDescriptor, name) { rootDescriptor?.get(name) } + var property: MutableMeta by remember { mutableStateOf(meta.getOrCreate(name)) } + var editorPropertyState: EditorPropertyState by remember { mutableStateOf(getPropertyState(name)) } + + + val keys = remember(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 = name.lastOrNull()?.toString() ?: "Properties" + + fun update() { + property = meta.getOrCreate(name) + editorPropertyState = getPropertyState(name) + } + + LaunchedEffect(meta) { + updates.collect { updatedName -> + if (updatedName == name) { + update() + } + } + } + + FlexRow({ + style { + alignItems(AlignItems.Center) + } + }) { + if (keys.isNotEmpty()) { + Span({ + classes(TreeStyles.treeCaret) + if (expanded) { + classes(TreeStyles.treeCaretDown) + } + onClick { expanded = !expanded } + }) + } + Span({ + classes(TreeStyles.treeLabel) + if (editorPropertyState != EditorPropertyState.Defined) { + classes(TreeStyles.treeLabelInactive) + } + }) { + Text(token) + } + + if (!name.isEmpty() && descriptor?.valueRequirement != ValueRequirement.ABSENT) { + Div({ + style { + width(160.px) + marginAll(1.px, 5.px) + } + }) { + ValueChooser(descriptor, editorPropertyState, property.value) { + property.value = it + editorPropertyState = getPropertyState(name) + } + } + + Button({ + classes(TreeStyles.propertyEditorButton) + if (editorPropertyState != EditorPropertyState.Defined) { + disabled() + } else { + onClick { + meta.remove(name) + update() + } + } + }) { + Text("\u00D7") + } + } + } + if (expanded) { + FlexColumn({ + classes(TreeStyles.tree) + }) { + keys.forEach { token -> + Div({ + classes(TreeStyles.treeItem) + }) { + PropertyEditorItem(meta, getPropertyState, scope, updates, name, descriptor, expanded) + } + } + } + } +} + +@Composable +public fun PropertyEditor( + scope: CoroutineScope, + properties: ObservableMutableMeta, + descriptor: MetaDescriptor? = null, + expanded: Boolean? = null, +) { + PropertyEditorItem( + meta = properties, + getPropertyState = { name -> + if (properties[name] != null) { + EditorPropertyState.Defined + } else if (descriptor?.get(name)?.defaultValue != null) { + EditorPropertyState.Default("descriptor") + } else { + EditorPropertyState.Undefined + } + }, + scope = scope, + updates = callbackFlow { + properties.onChange(scope) { name -> + scope.launch { + send(name) + } + } + + invokeOnClose { + properties.removeListener(scope) + } + }, + name = Name.EMPTY, + rootDescriptor = descriptor, + initialExpanded = expanded, + ) +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeCanvas.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeCanvas.kt new file mode 100644 index 00000000..9816b5c2 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeCanvas.kt @@ -0,0 +1,52 @@ +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import kotlinx.dom.clear +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.Div +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.dataforge.names.Name +import space.kscience.visionforge.solid.Solid +import space.kscience.visionforge.solid.specifications.Canvas3DOptions +import space.kscience.visionforge.solid.three.ThreeCanvas +import space.kscience.visionforge.solid.three.ThreePlugin + +@Composable +public fun ThreeCanvas( + context: Context, + options: Canvas3DOptions?, + solid: Solid?, + selected: Name?, +) { + + val three: ThreePlugin by derivedStateOf { context.request(ThreePlugin) } + + Div({ + style { + maxWidth(100.vw) + maxHeight(100.vh) + width(100.percent) + height(100.percent) + } + }) { + var canvas: ThreeCanvas? = null + DisposableEffect(options) { + canvas = ThreeCanvas(three, scopeElement, options ?: Canvas3DOptions()) + onDispose { + scopeElement.clear() + canvas = null + } + } + LaunchedEffect(solid) { + if (solid != null) { + canvas?.render(solid) + } else { + canvas?.clear() + } + } + LaunchedEffect(selected) { + canvas?.select(selected) + } + } +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeJS.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeJS.kt deleted file mode 100644 index 6033c712..00000000 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/ThreeJS.kt +++ /dev/null @@ -1,11 +0,0 @@ -package space.kscience.visionforge.compose - -import androidx.compose.material.Surface -import androidx.compose.runtime.Composable - -@Composable -public fun ThreeJs(){ - Surface { - - } -} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt index 29074464..de9001ba 100644 --- a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/TreeStyles.kt @@ -1,8 +1,10 @@ package space.kscience.visionforge.compose -import kotlinx.css.* +import org.jetbrains.compose.web.ExperimentalComposeWebApi import org.jetbrains.compose.web.css.* + +@OptIn(ExperimentalComposeWebApi::class) public object TreeStyles : StyleSheet() { /** * Remove default bullets @@ -16,12 +18,12 @@ public object TreeStyles : StyleSheet() { /** * Style the caret/arrow */ - public val treeCaret by style { + public val treeCaret: String by style { cursor("pointer") - userSelect = UserSelect.none + userSelect(UserSelect.none) /* Create the caret/arrow with a unicode, and style it */ before { - content = "\u25B6".quoted + content("\u25B6") color(Color.black) display(DisplayStyle.InlineBlock) marginRight(6.px) @@ -31,9 +33,9 @@ public object TreeStyles : StyleSheet() { /** * Rotate the caret/arrow icon when clicked on (using JavaScript) */ - public val treeCaredDown by style { + public val treeCaretDown: String by style { before { - content = "\u25B6".quoted + content("\u25B6") color(Color.black) display(DisplayStyle.InlineBlock) marginRight(6.px) @@ -45,7 +47,7 @@ public object TreeStyles : StyleSheet() { alignItems(AlignItems.Center) paddingLeft(10.px) border { - left{ + left { width(1.px) color(Color.lightgray) style = LineStyle.Dashed @@ -53,19 +55,40 @@ public object TreeStyles : StyleSheet() { } } - public val treeLabel by style { + public val treeLabel: String by style { border(style = LineStyle.None) - padding(left = 4.pt, right = 4.pt, top = 0.pt, bottom = 0.pt) + paddingAll(left = 4.pt, right = 4.pt) textAlign("left") flex(1) } - public val treeLabelInactive: RuleSet by css { - color = Color.lightGray + public val treeLabelInactive: String by style { + color(Color.lightgray) } - public val treeLabelSelected: RuleSet by css { - backgroundColor = Color.lightBlue + public val treeLabelSelected: String by style { + backgroundColor(Color.lightblue) + } + + public val propertyEditorButton: String by style { + width(24.px) + alignSelf(AlignSelf.Stretch) + marginAll(1.px, 5.px) + backgroundColor(Color.white) + border{ + style(LineStyle.Solid) + } + borderRadius(2.px) + textAlign("center") + textDecoration("none") + cursor("pointer") + disabled { + cursor("auto") + border{ + style(LineStyle.Dashed) + } + color(Color.lightgray) + } } } \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt new file mode 100644 index 00000000..9f6eb3e4 --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/css.kt @@ -0,0 +1,35 @@ +package space.kscience.visionforge.compose + +import org.jetbrains.compose.web.css.* + +public enum class UserSelect { + inherit, initial, revert, revertLayer, unset, + + none, auto, text, contain, all; +} + +public fun StyleScope.userSelect(value: UserSelect) { + property("user-select", value.name) +} + +public fun StyleScope.content(value: String) { + property("content", "'$value'") +} + +public fun StyleScope.paddingAll( + top: CSSNumeric = 0.pt, + right: CSSNumeric = top, + bottom: CSSNumeric = top, + left: CSSNumeric = right, +) { + padding(top, right, bottom, left) +} + +public fun StyleScope.marginAll( + top: CSSNumeric = 0.pt, + right: CSSNumeric = top, + bottom: CSSNumeric = top, + left: CSSNumeric = right, +) { + margin(top, right, bottom, left) +} \ No newline at end of file diff --git a/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt new file mode 100644 index 00000000..0be879ad --- /dev/null +++ b/ui/compose/src/jsMain/kotlin/space/kscience/visionforge/compose/valueChooser.kt @@ -0,0 +1,268 @@ +@file:Suppress("UNUSED_PARAMETER") + +package space.kscience.visionforge.compose + +import androidx.compose.runtime.* +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.css.percent +import org.jetbrains.compose.web.css.px +import org.jetbrains.compose.web.css.width +import org.jetbrains.compose.web.dom.Input +import org.jetbrains.compose.web.dom.Option +import org.jetbrains.compose.web.dom.Select +import org.jetbrains.compose.web.dom.Text +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLOptionElement +import org.w3c.dom.asList +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.meta.descriptors.MetaDescriptor +import space.kscience.dataforge.meta.descriptors.ValueRequirement +import space.kscience.dataforge.meta.descriptors.allowedValues +import space.kscience.visionforge.Colors +import space.kscience.visionforge.widgetType +import three.math.Color + + +@Composable +public fun StringValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + var stringValue by remember { mutableStateOf(value?.string ?: "") } + Input(type = InputType.Text) { + style { + width(100.percent) + } + value(stringValue) + onKeyDown { event -> + if (event.type == "keydown" && event.asDynamic().key == "Enter") { + stringValue = (event.target as HTMLInputElement).value + onValueChange(stringValue.asValue()) + } + } + onChange { + stringValue = it.target.value + } + } +} + + +@Composable +public fun BooleanValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + Input(type = InputType.Checkbox) { + style { + width(100.percent) + } + //this.attributes["indeterminate"] = (props.item == null).toString() + checked(value?.boolean ?: false) + + onChange { + val newValue = it.target.checked + onValueChange(newValue.asValue()) + } + } +} + +@Composable +public fun NumberValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + var innerValue by remember { mutableStateOf(value?.string ?: "") } + Input(type = InputType.Number) { + style { + width(100.percent) + } + value(innerValue) + onKeyDown { event -> + if (event.type == "keydown" && event.asDynamic().key == "Enter") { + innerValue = (event.target as HTMLInputElement).value + val number = innerValue.toDoubleOrNull() + if (number == null) { + console.error("The input value $innerValue is not a number") + } else { + onValueChange(number.asValue()) + } + } + } + onChange { + innerValue = it.target.value + } + descriptor?.attributes?.get("step").number?.let { + step(it) + } + descriptor?.attributes?.get("min").string?.let { + min(it) + } + descriptor?.attributes?.get("max").string?.let { + max(it) + } + } +} + + +@Composable +public fun ComboValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + var selected by remember { mutableStateOf(value?.string ?: "") } + Select({ + style { + width(100.percent) + } + onChange { + selected = it.target.value + onValueChange(selected.asValue()) + } + }, multiple = false) { + descriptor?.allowedValues?.forEach { + Option(it.string, { if (it == value) selected() }) { + Text(it.string) + } + } + + } +} + +@Composable +public fun ColorValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + Input(type = InputType.Color) { + style { + width(100.percent) + marginAll(0.px) + } + value( + value?.let { value -> + if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int) + else "#" + Color(value.string).getHexString() + } ?: "#000000" + ) + onChange { + onValueChange(it.target.value.asValue()) + } + } +} + + +@Composable +public fun MultiSelectChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + Select({ + onChange { event -> + val newSelected = event.target.selectedOptions.asList() + .map { (it as HTMLOptionElement).value.asValue() } + onValueChange(newSelected.asValue()) + + } + }, multiple = true) { + descriptor?.allowedValues?.forEach { optionValue -> + Option(optionValue.string, { + value?.list?.let { if (optionValue in it) selected() } + }) { + Text(optionValue.string) + } + } + + } +} + +@Composable +public fun RangeValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + var innerValue by remember { mutableStateOf(value?.double) } + var rangeDisabled: Boolean by remember { mutableStateOf(state != EditorPropertyState.Defined) } + + + FlexRow { + if (descriptor?.valueRequirement != ValueRequirement.REQUIRED) { + Input(type = InputType.Checkbox) { + if (!rangeDisabled) defaultChecked() + + onChange { + val checkBoxValue = it.target.checked + rangeDisabled = !checkBoxValue + onValueChange( + if (!checkBoxValue) { + null + } else { + innerValue?.asValue() + } + ) + } + } + } + } + + Input(type = InputType.Range) { + style { + width(100.percent) + } + if (rangeDisabled) disabled() + value(innerValue?.toString() ?: "") + onChange { + val newValue = it.target.value + onValueChange(newValue.toDoubleOrNull()?.asValue()) + innerValue = newValue.toDoubleOrNull() + } + descriptor?.attributes?.get("min").string?.let { + min(it) + } + descriptor?.attributes?.get("max").string?.let { + max(it) + } + descriptor?.attributes?.get("step").number?.let { + step(it) + } + + } + +} + +@Composable +public fun ValueChooser( + descriptor: MetaDescriptor?, + state: EditorPropertyState, + value: Value?, + onValueChange: (Value?) -> Unit, +) { + val rawInput by remember { mutableStateOf(false) } + + val type = descriptor?.valueTypes?.firstOrNull() + + when { + rawInput -> StringValueChooser(descriptor, state, value, onValueChange) + descriptor?.widgetType == "color" -> ColorValueChooser(descriptor, state, value, onValueChange) + descriptor?.widgetType == "multiSelect" -> MultiSelectChooser(descriptor, state, value, onValueChange) + descriptor?.widgetType == "range" -> RangeValueChooser(descriptor, state, value, onValueChange) + type == ValueType.BOOLEAN -> BooleanValueChooser(descriptor, state, value, onValueChange) + type == ValueType.NUMBER -> NumberValueChooser(descriptor, state, value, onValueChange) + descriptor?.allowedValues?.isNotEmpty() ?: false -> ComboValueChooser(descriptor, state, value, onValueChange) + //TODO handle lists + else -> StringValueChooser(descriptor, state, value, onValueChange) + } +} \ No newline at end of file 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 55d1391b..b4e6cba7 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -47,7 +47,7 @@ public interface Vision : Described { } public fun onMetaEvent(meta: Meta){ - + //Do nothing by default } /**