From fdc221dfa130239350163bae74d8ab8e76b5168d Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 9 Nov 2020 19:51:57 +0300 Subject: [PATCH] controls redesign --- .../vision/gdml/demo/GDMLAppComponent.kt | 137 ++++--- demo/gdml/src/jsMain/resources/index.html | 2 +- demo/muon-monitor/build.gradle.kts | 2 +- .../mipt/npm/muon/monitor/MMAppComponent.kt | 16 +- .../ru/mipt/npm/muon/monitor/MMDemoApp.kt | 6 +- playground/src/jsMain/kotlin/PlayGroundApp.kt | 2 +- .../dataforge/vision/bootstrap/bootstrap.kt | 338 ++++++------------ .../vision/bootstrap/outputConfig.kt | 189 ++++------ .../vision/bootstrap/reactBootstrap.kt | 182 ++++++++++ .../vision/bootstrap/tabComponent.kt | 95 +++++ .../vision/bootstrap/threeControls.kt | 81 +++++ ui/react/build.gradle.kts | 3 +- .../vision/react}/ThreeCanvasComponent.kt | 58 +-- .../hep/dataforge/vision/react/TreeStyles.kt | 2 +- visionforge-core/build.gradle.kts | 2 +- .../solid/specifications/Canvas3DOptions.kt | 10 +- .../vision/solid/three/DatCanvasControls.kt | 5 + .../vision/solid/three/ThreeCanvas.kt | 50 ++- .../hep/dataforge/vision/solid/three/three.kt | 9 +- 19 files changed, 687 insertions(+), 502 deletions(-) create mode 100644 ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/reactBootstrap.kt create mode 100644 ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/tabComponent.kt create mode 100644 ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt rename {visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three => ui/react/src/main/kotlin/hep/dataforge/vision/react}/ThreeCanvasComponent.kt (52%) create mode 100644 visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/DatCanvasControls.kt 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 828641d9..f2c4864c 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 @@ -2,29 +2,31 @@ package hep.dataforge.vision.gdml.demo import hep.dataforge.context.Context import hep.dataforge.names.Name -import hep.dataforge.names.isEmpty import hep.dataforge.vision.Vision -import hep.dataforge.vision.VisionGroup -import hep.dataforge.vision.bootstrap.* +import hep.dataforge.vision.bootstrap.card +import hep.dataforge.vision.bootstrap.nameCrumbs +import hep.dataforge.vision.bootstrap.threeControls import hep.dataforge.vision.gdml.toVision -import hep.dataforge.vision.react.objectTree +import hep.dataforge.vision.react.ThreeCanvasComponent +import hep.dataforge.vision.react.flexColumn +import hep.dataforge.vision.react.flexRow import hep.dataforge.vision.solid.Solid -import hep.dataforge.vision.solid.SolidGroup import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.three.ThreeCanvas -import hep.dataforge.vision.solid.three.ThreeCanvasComponent import kotlinx.browser.window -import kotlinx.css.FlexBasis -import kotlinx.css.Overflow -import kotlinx.css.flex -import kotlinx.css.overflow +import kotlinx.css.* +import kotlinx.css.properties.border import kscience.gdml.GDML import kscience.gdml.decodeFromString import org.w3c.files.FileReader import org.w3c.files.get -import react.* +import react.RProps +import react.child import react.dom.h1 +import react.functionalComponent +import react.useState import styled.css +import styled.styledDiv external interface GDMLAppProps : RProps { var context: Context @@ -46,7 +48,7 @@ val GDMLApp = functionalComponent("GDMLApp") { props -> var canvas: ThreeCanvas? by useState { null } var vision: Vision? by useState { props.rootObject } - val select: (Name?) -> Unit = { + val onSelect: (Name?) -> Unit = { selected = it } @@ -67,20 +69,56 @@ val GDMLApp = functionalComponent("GDMLApp") { props -> vision = parsedVision } - gridColumn { + flexColumn { css { - flex(1.0, 1.0, FlexBasis.auto) + height = 100.pct + width = 100.pct } - h1 { +"GDML/JSON loader demo" } - gridRow { + styledDiv { css { - +"p-1" - overflow = Overflow.auto + +"page-header" + +"justify-content-center" } - gridColumn(3, maxSize = GridMaxSize.XL) { + h1 { +"GDML/JSON loader demo" } + } + flexRow { + css { + flex(1.0, 1.0, FlexBasis.auto) + flexWrap = FlexWrap.wrap + } + + flexColumn { css { - +"order-2" - +"order-xl-1" + flex(1.0, 1.0, FlexBasis.auto) + margin(10.px) + } + nameCrumbs(selected, "World", onSelect) + + //canvas + styledDiv { + css { + flex(1.0, 1.0, FlexBasis.auto) + } + child(ThreeCanvasComponent) { + attrs { + this.context = props.context + this.obj = vision as? Solid + this.selected = selected + this.clickCallback = onSelect + this.canvasCallback = { + canvas = it + } + } + } + } + } + flexColumn { + css { + minWidth = 500.px + maxHeight = 100.pct + flex(1.0, 1.0, FlexBasis.zero) + margin(left = 4.px, right = 4.px) + border(1.px, BorderStyle.solid, Color.lightGray) } card("Load data") { fileDrop("(drag file here)") { files -> @@ -97,61 +135,8 @@ val GDMLApp = functionalComponent("GDMLApp") { props -> } } } - //tree - card("Object tree") { - vision?.let { - objectTree(it, selected, select) - } - } - } - - gridColumn(6, maxSize = GridMaxSize.XL) { - css { - +"order-1" - +"order-xl-2" - } - //canvas - child(ThreeCanvasComponent) { - attrs { - this.context = props.context - this.obj = vision as? Solid - this.selected = selected - this.clickCallback = select - this.canvasCallback = { - canvas = it - } - } - } - } - gridColumn(3, maxSize = GridMaxSize.XL) { - css { - +"order-3" - } - container { - //settings - canvas?.let { - card("Canvas configuration") { - canvasControls(it) - } - } - } - container { - //properties - namecrumbs(selected, "World") { selected = it } - selected.let { selected -> - val selectedObject: Vision? = when { - selected == null -> null - selected.isEmpty() -> vision - else -> (vision as? VisionGroup)?.get(selected) - } - if (selectedObject != null) { - visionPropertyEditor( - selectedObject, - default = selectedObject.getAllProperties(), - key = selected - ) - } - } + canvas?.let { + threeControls(it, selected, onSelect) } } } diff --git a/demo/gdml/src/jsMain/resources/index.html b/demo/gdml/src/jsMain/resources/index.html index 14bb3178..74e27a96 100644 --- a/demo/gdml/src/jsMain/resources/index.html +++ b/demo/gdml/src/jsMain/resources/index.html @@ -2,7 +2,7 @@ - + Three js demo for particle physics diff --git a/demo/muon-monitor/build.gradle.kts b/demo/muon-monitor/build.gradle.kts index 1f44169d..ba7092f0 100644 --- a/demo/muon-monitor/build.gradle.kts +++ b/demo/muon-monitor/build.gradle.kts @@ -48,7 +48,7 @@ kotlin { dependencies { implementation(project(":ui:bootstrap")) implementation("io.ktor:ktor-client-js:$ktorVersion") - implementation("io.ktor:ktor-client-serialization-js:$ktorVersion") + implementation("io.ktor:ktor-client-serialization:$ktorVersion") } } } 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 527c2b77..9917d75a 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 @@ -8,19 +8,24 @@ import hep.dataforge.names.length import hep.dataforge.vision.Vision import hep.dataforge.vision.bootstrap.canvasControls import hep.dataforge.vision.bootstrap.card +import hep.dataforge.vision.react.ThreeCanvasComponent import hep.dataforge.vision.react.configEditor import hep.dataforge.vision.react.objectTree import hep.dataforge.vision.solid.specifications.Camera import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.solid.three.ThreeCanvas -import hep.dataforge.vision.solid.three.ThreeCanvasComponent import io.ktor.client.HttpClient import io.ktor.client.request.get import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import kotlinx.css.height +import kotlinx.css.pct import kotlinx.html.js.onClickFunction -import react.* +import react.RProps +import react.child import react.dom.* +import react.functionalComponent +import react.useState import styled.css import styled.styledDiv import kotlin.math.PI @@ -62,13 +67,18 @@ val MMApp = functionalComponent("Muon monitor") { props -> +"col-lg-3" +"px-0" +"overflow-auto" + +"h-100" } //tree card("Object tree") { objectTree(root, selected, select) } } - div("col-lg-6") { + styledDiv { + css { + +"col-lg-6" + height = 100.pct + } //canvas child(ThreeCanvasComponent) { attrs { 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 282050d2..2fabd0d8 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 @@ -3,12 +3,10 @@ package ru.mipt.npm.muon.monitor import hep.dataforge.context.Global import hep.dataforge.js.Application import hep.dataforge.js.startApplication -import hep.dataforge.vision.solid.SolidManager import io.ktor.client.HttpClient import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.serializer.KotlinxSerializer import kotlinx.browser.document -import kotlinx.serialization.json.Json import react.child import react.dom.div import react.dom.render @@ -19,12 +17,10 @@ private class MMDemoApp : Application { private val connection = HttpClient { install(JsonFeature) { - serializer = KotlinxSerializer(Json { serializersModule = SolidManager.serializersModuleForSolids }) + serializer = KotlinxSerializer() } } - //TODO introduce react application - override fun start(state: Map) { val context = Global.context("demo") {} diff --git a/playground/src/jsMain/kotlin/PlayGroundApp.kt b/playground/src/jsMain/kotlin/PlayGroundApp.kt index 72196672..4542a9e9 100644 --- a/playground/src/jsMain/kotlin/PlayGroundApp.kt +++ b/playground/src/jsMain/kotlin/PlayGroundApp.kt @@ -2,10 +2,10 @@ import hep.dataforge.context.Global import hep.dataforge.js.Application import hep.dataforge.js.startApplication import hep.dataforge.vision.bootstrap.visionPropertyEditor +import hep.dataforge.vision.react.ThreeCanvasComponent import hep.dataforge.vision.react.objectTree import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.specifications.Canvas3DOptions -import hep.dataforge.vision.solid.three.ThreeCanvasComponent import hep.dataforge.vision.solid.three.ThreePlugin import kotlinx.browser.document import org.w3c.dom.HTMLElement diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/bootstrap.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/bootstrap.kt index 14efcccb..9ebdca64 100644 --- a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/bootstrap.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/bootstrap.kt @@ -1,238 +1,116 @@ package hep.dataforge.vision.bootstrap -import hep.dataforge.names.Name -import hep.dataforge.names.NameToken -import hep.dataforge.names.length -import hep.dataforge.vision.Vision -import hep.dataforge.vision.react.ObjectTree -import kotlinx.html.* -import kotlinx.html.js.div -import kotlinx.html.js.onClickFunction -import org.w3c.dom.Element -import org.w3c.dom.HTMLElement -import react.RBuilder -import react.ReactElement -import react.child -import react.dom.* -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv +//public inline fun TagConsumer.card(title: String, crossinline block: TagConsumer.() -> Unit) { +// div("card w-100") { +// div("card-body") { +// h3(classes = "card-title") { +title } +// block() +// } +// } +//} -public inline fun TagConsumer.card(title: String, crossinline block: TagConsumer.() -> Unit) { - div("card w-100") { - div("card-body") { - h3(classes = "card-title") { +title } - block() - } - } -} - -public inline fun RBuilder.card(title: String, classes: String? = null, crossinline block: RBuilder.() -> Unit) { - div("card w-100 $classes") { - div("card-body") { - h3(classes = "card-title") { - +title - } - block() - } - } -} - -public fun TagConsumer.accordion(id: String, elements: List Unit>>) { - div("container-fluid") { - div("accordion") { - this.id = id - elements.forEachIndexed { index, (title, builder) -> - val headerID = "${id}-${index}-heading" - val collapseID = "${id}-${index}-collapse" - div("card") { - div("card-header") { - this.id = headerID - h5("mb-0") { - button(classes = "btn btn-link collapsed", type = ButtonType.button) { - attributes["data-toggle"] = "collapse" - attributes["data-target"] = "#$collapseID" - attributes["aria-expanded"] = "false" - attributes["aria-controls"] = collapseID - +title - } - } - } - div("collapse") { - this.id = collapseID - attributes["aria-labelledby"] = headerID - attributes["data-parent"] = "#$id" - div("card-body", block = builder) - } - } - } - } - } -} - -public typealias AccordionBuilder = MutableList Unit>> - -public fun AccordionBuilder.entry(title: String, builder: DIV.() -> Unit) { - add(title to builder) -} - -public fun TagConsumer.accordion(id: String, builder: AccordionBuilder.() -> Unit) { - val list = ArrayList Unit>>().apply(builder) - accordion(id, list) -} - -public fun RBuilder.accordion(id: String, elements: List.() -> Unit>>): ReactElement { - return div("container-fluid") { - div("accordion") { - attrs { - this.id = id - } - elements.forEachIndexed { index, (title, builder) -> - val headerID = "${id}-${index}-heading" - val collapseID = "${id}-${index}-collapse" - div("card p-0 m-0") { - div("card-header") { - attrs { - this.id = headerID - } - h5("mb-0") { - button(classes = "btn btn-link collapsed", type = ButtonType.button) { - attrs { - attributes["data-toggle"] = "collapse" - attributes["data-target"] = "#$collapseID" - attributes["aria-expanded"] = "false" - attributes["aria-controls"] = collapseID - } - +title - } - } - } - div("collapse") { - attrs { - this.id = collapseID - attributes["aria-labelledby"] = headerID - attributes["data-parent"] = "#$id" - } - div("card-body", block = builder) - } - } - } - } - } -} - -public fun RBuilder.namecrumbs(name: Name?, rootTitle: String, link: (Name) -> Unit) { - div("container-fluid p-0") { - nav { - attrs { - attributes["aria-label"] = "breadcrumb" - } - ol("breadcrumb") { - li("breadcrumb-item") { - button(classes = "btn btn-link p-0") { - +rootTitle - attrs { - onClickFunction = { - link(Name.EMPTY) - } - } - } - } - if (name != null) { - val tokens = ArrayList(name.length) - name.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") - link(fullName) - } - } - } - } - } - } - } - } - } -} - -public typealias RAccordionBuilder = MutableList.() -> Unit>> - -public fun RAccordionBuilder.entry(title: String, builder: RDOMBuilder
.() -> Unit) { - add(title to builder) -} - -public fun RBuilder.accordion(id: String, builder: RAccordionBuilder.() -> Unit): ReactElement { - val list = ArrayList.() -> Unit>>().apply(builder) - return accordion(id, list) -} - -public enum class ContainerSize(public val suffix: String) { - DEFAULT(""), - SM("-sm"), - MD("-md"), - LG("-lg"), - XL("-xl"), - FLUID("-fluid") -} - -public inline fun RBuilder.container( - size: ContainerSize = ContainerSize.FLUID, - block: StyledDOMBuilder
.() -> Unit -): ReactElement = styledDiv{ - css{ - classes.add("container${size.suffix}") - } - block() -} +//public typealias SectionsBuilder = MutableList Unit>> +// +//public fun SectionsBuilder.entry(title: String, builder: DIV.() -> Unit) { +// add(title to builder) +//} -public enum class GridMaxSize(public val suffix: String) { - NONE(""), - SM("-sm"), - MD("-md"), - LG("-lg"), - XL("-xl") -} +//public fun TagConsumer.accordion(id: String, elements: List Unit>>) { +// div("container-fluid") { +// div("accordion") { +// this.id = id +// elements.forEachIndexed { index, (title, builder) -> +// val headerID = "${id}-${index}-heading" +// val collapseID = "${id}-${index}-collapse" +// div("card") { +// div("card-header") { +// this.id = headerID +// h5("mb-0") { +// button(classes = "btn btn-link collapsed", type = ButtonType.button) { +// attributes["data-toggle"] = "collapse" +// attributes["data-target"] = "#$collapseID" +// attributes["aria-expanded"] = "false" +// attributes["aria-controls"] = collapseID +// +title +// } +// } +// } +// div("collapse") { +// this.id = collapseID +// attributes["aria-labelledby"] = headerID +// attributes["data-parent"] = "#$id" +// div("card-body", block = builder) +// } +// } +// } +// } +// } +//} -public inline fun RBuilder.gridColumn( - weight: Int? = null, - maxSize: GridMaxSize = GridMaxSize.NONE, - block: StyledDOMBuilder
.() -> Unit -): ReactElement = styledDiv { - val weightSuffix = weight?.let { "-$it" } ?: "" - css { - classes.add("col${maxSize.suffix}$weightSuffix") - } - block() -} -public inline fun RBuilder.gridRow( - block: StyledDOMBuilder
.() -> Unit -): ReactElement = styledDiv{ - css{ - classes.add("row") - } - block() -} +//public fun TagConsumer.accordion(id: String, builder: AccordionBuilder.() -> Unit) { +// val list = ArrayList Unit>>().apply(builder) +// accordion(id, list) +//} -public fun Element.renderObjectTree( - vision: Vision, - clickCallback: (Name) -> Unit = {} -): Unit = render(this) { - card("Object tree") { - child(ObjectTree) { - attrs { - this.name = Name.EMPTY - this.obj = vision - this.selected = null - this.clickCallback = clickCallback - } - } - } -} \ No newline at end of file +//public fun Element.displayCanvasControls(canvas: ThreeCanvas, block: TagConsumer.() -> Unit = {}) { +// clear() +// append { +// accordion("controls") { +// entry("Settings") { +// div("row") { +// div("col-2") { +// label("checkbox-inline") { +// input(type = InputType.checkBox) { +// checked = canvas.axes.visible +// onChangeFunction = { +// canvas.axes.visible = checked +// } +// } +// +"Axes" +// } +// } +// div("col-1") { +// button { +// +"Export" +// onClickFunction = { +// val json = (canvas.content as? SolidGroup)?.let { group -> +// val visionManager = canvas.context.plugins.fetch(SolidManager).visionManager +// visionManager.encodeToString(group) +// } +// if (json != null) { +// saveData(it, "object.json", "text/json") { +// json +// } +// } +// } +// } +// } +// } +// } +// entry("Layers") { +// div("row") { +// (0..11).forEach { layer -> +// div("col-1") { +// label { +layer.toString() } +// input(type = InputType.checkBox) { +// if (layer == 0) { +// checked = true +// } +// onChangeFunction = { +// if (checked) { +// canvas.camera.layers.enable(layer) +// } else { +// canvas.camera.layers.disable(layer) +// } +// } +// } +// } +// } +// } +// } +// } +// block() +// } +//} \ No newline at end of file diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/outputConfig.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/outputConfig.kt index 4d039b92..3a64ef7f 100644 --- a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/outputConfig.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/outputConfig.kt @@ -1,24 +1,26 @@ package hep.dataforge.vision.bootstrap +import hep.dataforge.vision.react.flexColumn +import hep.dataforge.vision.react.flexRow import hep.dataforge.vision.solid.SolidGroup import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.three.ThreeCanvas -import kotlinx.dom.clear -import kotlinx.html.* -import kotlinx.html.dom.append +import kotlinx.css.* +import kotlinx.css.properties.border +import kotlinx.html.InputType import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onClickFunction -import org.w3c.dom.Element -import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLInputElement import org.w3c.dom.events.Event import org.w3c.files.Blob import org.w3c.files.BlobPropertyBag import react.* import react.dom.button -import react.dom.div +import react.dom.h3 import react.dom.input import react.dom.label +import styled.css +import styled.styledDiv private fun saveData(event: Event, fileName: String, mimeType: String = "text/plain", dataBuilder: () -> String) { event.stopPropagation(); @@ -30,8 +32,8 @@ private fun saveData(event: Event, fileName: String, mimeType: String = "text/pl } public fun RBuilder.canvasControls(canvas: ThreeCanvas): ReactElement { - return child(CanvasControls){ - attrs{ + return child(CanvasControls) { + attrs { this.canvas = canvas } } @@ -41,39 +43,75 @@ public external interface CanvasControlsProps : RProps { public var canvas: ThreeCanvas } -public val CanvasControls: FunctionalComponent = functionalComponent ("CanvasControls") { props -> +public val CanvasControls: FunctionalComponent = functionalComponent("CanvasControls") { props -> val visionManager = useMemo( { props.canvas.context.plugins.fetch(SolidManager).visionManager }, arrayOf(props.canvas) ) - accordion("controls") { - entry("Settings") { - div("row") { - div("col-2") { - label("checkbox-inline") { - input(type = InputType.checkBox) { - attrs { - defaultChecked = props.canvas.axes.visible - onChangeFunction = { - props.canvas.axes.visible = (it.target as HTMLInputElement).checked - } - } + flexColumn { + h3 { +"Axes" } + flexRow { + css{ + border(1.px,BorderStyle.solid, Color.blue) + padding(4.px) + } + label("checkbox-inline") { + input(type = InputType.checkBox) { + attrs { + defaultChecked = props.canvas.axes.visible + onChangeFunction = { + props.canvas.axes.visible = (it.target as HTMLInputElement).checked } - +"Axes" } } - div("col-1") { - button { - +"Export" + +"Axes" + } + } + h3 { +"Export" } + flexRow { + css{ + border(1.px,BorderStyle.solid, Color.blue) + padding(4.px) + } + button { + +"Export" + attrs { + onClickFunction = { + val json = (props.canvas.content as? SolidGroup)?.let { group -> + visionManager.encodeToString(group) + } + if (json != null) { + saveData(it, "object.json", "text/json") { + json + } + } + } + } + } + } + h3 { +"Layers" } + flexRow { + css { + flexWrap = FlexWrap.wrap + border(1.px,BorderStyle.solid, Color.blue) + padding(4.px) + } + (0..31).forEach { layer -> + styledDiv { + css{ + padding(4.px) + } + label { +layer.toString() } + input(type = InputType.checkBox) { attrs { - onClickFunction = { - val json = (props.canvas.content as? SolidGroup)?.let { group -> - visionManager.encodeToString(group) - } - if (json != null) { - saveData(it, "object.json", "text/json") { - json - } + if (layer == 0) { + defaultChecked = true + } + onChangeFunction = { + if ((it.target as HTMLInputElement).checked) { + props.canvas.camera.layers.enable(layer) + } else { + props.canvas.camera.layers.disable(layer) } } } @@ -81,90 +119,5 @@ public val CanvasControls: FunctionalComponent = functional } } } - entry("Layers") { - div("row") { - (0..11).forEach { layer -> - div("col-1") { - label { +layer.toString() } - input(type = InputType.checkBox) { - attrs { - if (layer == 0) { - defaultChecked = true - } - onChangeFunction = { - if ((it.target as HTMLInputElement).checked) { - props.canvas.camera.layers.enable(layer) - } else { - props.canvas.camera.layers.disable(layer) - } - } - } - } - } - } - } - } - } -} - - -public fun Element.displayCanvasControls(canvas: ThreeCanvas, block: TagConsumer.() -> Unit = {}) { - clear() - append { - accordion("controls") { - entry("Settings") { - div("row") { - div("col-2") { - label("checkbox-inline") { - input(type = InputType.checkBox) { - checked = canvas.axes.visible - onChangeFunction = { - canvas.axes.visible = checked - } - } - +"Axes" - } - } - div("col-1") { - button { - +"Export" - onClickFunction = { - val json = (canvas.content as? SolidGroup)?.let { group -> - val visionManager = canvas.context.plugins.fetch(SolidManager).visionManager - visionManager.encodeToString(group) - } - if (json != null) { - saveData(it, "object.json", "text/json") { - json - } - } - } - } - } - } - } - entry("Layers") { - div("row") { - (0..11).forEach { layer -> - div("col-1") { - label { +layer.toString() } - input(type = InputType.checkBox) { - if (layer == 0) { - checked = true - } - onChangeFunction = { - if (checked) { - canvas.camera.layers.enable(layer) - } else { - canvas.camera.layers.disable(layer) - } - } - } - } - } - } - } - } - block() } } \ No newline at end of file diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/reactBootstrap.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/reactBootstrap.kt new file mode 100644 index 00000000..d17dcac3 --- /dev/null +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/reactBootstrap.kt @@ -0,0 +1,182 @@ +package hep.dataforge.vision.bootstrap + +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken +import hep.dataforge.names.length +import kotlinx.html.ButtonType +import kotlinx.html.DIV +import kotlinx.html.id +import kotlinx.html.js.onClickFunction +import react.RBuilder +import react.ReactElement +import react.dom.* +import styled.StyledDOMBuilder +import styled.css +import styled.styledDiv +import styled.styledNav + + +public inline fun RBuilder.card(title: String, crossinline block: StyledDOMBuilder
.() -> Unit): ReactElement = + styledDiv { + css { + +"card" + +"w-100" + } + styledDiv { + css { + +"card-body" + } + h3(classes = "card-title") { + +title + } + block() + } + } + +public fun RBuilder.accordion( + id: String, + elements: List.() -> Unit>>, +): ReactElement = styledDiv { + css { + +"accordion" + //+"p-1" + } + attrs { + this.id = id + } + elements.forEachIndexed { index, (title, builder) -> + val headerID = "${id}-${index}-heading" + val collapseID = "${id}-${index}-collapse" + div("card p-0 m-0") { + div("card-header") { + attrs { + this.id = headerID + } + h5("mb-0") { + button(classes = "btn btn-link collapsed", type = ButtonType.button) { + attrs { + attributes["data-toggle"] = "collapse" + attributes["data-target"] = "#$collapseID" + attributes["aria-expanded"] = "false" + attributes["aria-controls"] = collapseID + } + +title + } + } + } + div("collapse") { + attrs { + this.id = collapseID + attributes["aria-labelledby"] = headerID + attributes["data-parent"] = "#$id" + } + styledDiv { + css { + +"card-body" + } + builder() + } + } + } + } +} + + +public fun RBuilder.nameCrumbs(name: Name?, rootTitle: String, link: (Name) -> Unit): ReactElement = styledNav { + css { + +"p-0" + } + attrs { + attributes["aria-label"] = "breadcrumb" + } + ol("breadcrumb") { + li("breadcrumb-item") { + button(classes = "btn btn-link p-0") { + +rootTitle + attrs { + onClickFunction = { + link(Name.EMPTY) + } + } + } + } + if (name != null) { + val tokens = ArrayList(name.length) + name.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") + link(fullName) + } + } + } + } + } + } + } +} + +public typealias RSectionsBuilder = MutableList.() -> Unit>> + +public fun RSectionsBuilder.entry(title: String, builder: StyledDOMBuilder
.() -> Unit) { + add(title to builder) +} + +public fun RBuilder.accordion(id: String, builder: RSectionsBuilder.() -> Unit): ReactElement { + val list = ArrayList.() -> Unit>>().apply(builder) + return accordion(id, list) +} + +public enum class ContainerSize(public val suffix: String) { + DEFAULT(""), + SM("-sm"), + MD("-md"), + LG("-lg"), + XL("-xl"), + FLUID("-fluid") +} + +public inline fun RBuilder.container( + size: ContainerSize = ContainerSize.FLUID, + block: StyledDOMBuilder
.() -> Unit, +): ReactElement = styledDiv { + css { + classes.add("container${size.suffix}") + } + block() +} + + +public enum class GridMaxSize(public val suffix: String) { + NONE(""), + SM("-sm"), + MD("-md"), + LG("-lg"), + XL("-xl") +} + +public inline fun RBuilder.gridColumn( + weight: Int? = null, + maxSize: GridMaxSize = GridMaxSize.NONE, + block: StyledDOMBuilder
.() -> Unit, +): ReactElement = styledDiv { + val weightSuffix = weight?.let { "-$it" } ?: "" + css { + classes.add("col${maxSize.suffix}$weightSuffix") + } + block() +} + +public inline fun RBuilder.gridRow( + block: StyledDOMBuilder
.() -> Unit, +): ReactElement = styledDiv { + css { + classes.add("row") + } + block() +} \ No newline at end of file diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/tabComponent.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/tabComponent.kt new file mode 100644 index 00000000..5580b923 --- /dev/null +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/tabComponent.kt @@ -0,0 +1,95 @@ +package hep.dataforge.vision.bootstrap + +import hep.dataforge.vision.react.flexColumn +import kotlinx.css.Overflow +import kotlinx.css.flexGrow +import kotlinx.css.overflowY +import kotlinx.html.DIV +import kotlinx.html.classes +import kotlinx.html.js.onClickFunction +import react.* +import react.dom.button +import react.dom.li +import react.dom.ul +import styled.StyledDOMBuilder +import styled.css +import styled.styledDiv + +public external class TabProps : RProps { + public var id: String + public var title: String? +} + +@JsExport +public val Tab: FunctionalComponent = functionalComponent { props -> + props.children() +} + +public external class TabPaneProps : RProps { + public var activeTab: String? +} + +@JsExport +public val TabPane: FunctionalComponent = functionalComponent("TabPane") { props -> + var activeTab: String? by useState(props.activeTab) + + val children: Array = Children.map(props.children) { + it.asElementOrNull() + } ?: emptyArray() + + val childrenProps = children.mapNotNull { + it?.props?.unsafeCast() + } + + flexColumn { + css { + flexGrow = 1.0 + } + ul("nav nav-tabs") { + childrenProps.forEach { cp -> + li("nav-item") { + button(classes = "nav-link") { + +(cp.title ?: cp.id) + attrs { + if (cp.id == activeTab) { + classes += "active" + } + onClickFunction = { + activeTab = cp.id + } + } + } + } + } + } + children.find { (it?.props?.unsafeCast())?.id == activeTab }?.let { + child(it) + } + } +} + +public class TabBuilder(internal val parentBuilder: RBuilder) { + public fun tab(id: String, title: String? = null, builder: StyledDOMBuilder
.() -> Unit) { + parentBuilder.child(Tab) { + attrs { + this.id = id + this.title = title + } + styledDiv { + css { + overflowY = Overflow.auto + } + builder() + } + } + } +} + +public inline fun RBuilder.tabPane(activeTab: String? = null, crossinline builder: TabBuilder.() -> Unit) { + child(TabPane) { + attrs { + this.activeTab = activeTab + } + TabBuilder(this).builder() + } +} 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 new file mode 100644 index 00000000..ac0b5c33 --- /dev/null +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vision/bootstrap/threeControls.kt @@ -0,0 +1,81 @@ +package hep.dataforge.vision.bootstrap + +import hep.dataforge.names.Name +import hep.dataforge.names.isEmpty +import hep.dataforge.vision.Vision +import hep.dataforge.vision.VisionGroup +import hep.dataforge.vision.react.objectTree +import hep.dataforge.vision.solid.three.ThreeCanvas +import kotlinx.css.* +import kotlinx.css.properties.border +import react.* +import react.dom.h2 +import styled.css +import styled.styledDiv + +public external interface ThreeControlsProps : RProps { + public var canvas: ThreeCanvas + public var selected: Name? + public var onSelect: (Name) -> Unit +} + +@JsExport +public val ThreeControls: FunctionalComponent = functionalComponent { props -> + val vision = props.canvas.content + tabPane { + tab("Canvas") { + card("Canvas configuration") { + canvasControls(props.canvas) + } + } + tab("Tree") { + css { + border(1.px, BorderStyle.solid, Color.lightGray) + padding(10.px) + } + h2 { +"Object tree" } + styledDiv { + css { + overflowY = Overflow.auto + flex(1.0, 0.0, FlexBasis.auto) + } + props.canvas.content?.let { + objectTree(it, props.selected, props.onSelect) + } + } + } + tab("Properties") { + props.selected.let { selected -> + val selectedObject: Vision? = when { + selected == null -> null + selected.isEmpty() -> vision + else -> (vision as? VisionGroup)?.get(selected) + } + if (selectedObject != null) { + visionPropertyEditor( + selectedObject, + default = selectedObject.getAllProperties(), + key = selected + ) + } + } + } + this.parentBuilder.run { + props.children() + } + } +} + +public fun RBuilder.threeControls( + canvas: ThreeCanvas, + selected: Name?, + onSelect: (Name) -> Unit = {}, + builder: TabBuilder.() -> Unit = {} +): ReactElement = child(ThreeControls) { + attrs { + this.canvas = canvas + this.selected = selected + this.onSelect = onSelect + } + TabBuilder(this).builder() +} \ No newline at end of file diff --git a/ui/react/build.gradle.kts b/ui/react/build.gradle.kts index 9389bd35..b57cba20 100644 --- a/ui/react/build.gradle.kts +++ b/ui/react/build.gradle.kts @@ -6,6 +6,7 @@ val reactVersion by extra("17.0.0") val kotlinWrappersVersion: String by rootProject.extra dependencies{ - api(project(":visionforge-core")) + api(project(":visionforge-solid")) + api("org.jetbrains:kotlin-styled:5.2.0-$kotlinWrappersVersion") api("org.jetbrains:kotlin-react-dom:$reactVersion-$kotlinWrappersVersion") } \ No newline at end of file diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeCanvasComponent.kt b/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt similarity index 52% rename from visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeCanvasComponent.kt rename to ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt index 9f335642..5d961741 100644 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/ThreeCanvasComponent.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vision/react/ThreeCanvasComponent.kt @@ -1,13 +1,18 @@ -package hep.dataforge.vision.solid.three +package hep.dataforge.vision.react import hep.dataforge.context.Context import hep.dataforge.names.Name import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.specifications.Canvas3DOptions +import hep.dataforge.vision.solid.three.ThreeCanvas +import hep.dataforge.vision.solid.three.ThreePlugin +import hep.dataforge.vision.solid.three.output +import kotlinx.css.* import org.w3c.dom.Element import org.w3c.dom.HTMLElement import react.* -import react.dom.div +import styled.css +import styled.styledDiv public external interface ThreeCanvasProps : RProps { public var context: Context @@ -33,7 +38,8 @@ public val ThreeCanvasComponent: FunctionalComponent = functio if (canvas == null) { val element = elementRef.current as? HTMLElement ?: error("Canvas element not found") val three: ThreePlugin = props.context.plugins.fetch(ThreePlugin) - val newCanvas = three.output(element, props.options ?: Canvas3DOptions.empty(), props.clickCallback) + val newCanvas: ThreeCanvas = + three.output(element, props.options ?: Canvas3DOptions.empty(), props.clickCallback) props.canvasCallback?.invoke(newCanvas) canvas = newCanvas } @@ -51,43 +57,13 @@ public val ThreeCanvasComponent: FunctionalComponent = functio canvas?.select(props.selected) } - div { + styledDiv { + css { + minWidth = 500.px + minHeight = 500.px + display = Display.inherit + flex(1.0, 1.0, FlexBasis.auto) + } ref = elementRef } -} - -//public class ThreeCanvasComponent : RComponent() { -// -// private var canvas: ThreeCanvas? = null -// -// override fun componentDidMount() { -// props.obj?.let { obj -> -// if (canvas == null) { -// val element = state.element as? HTMLElement ?: error("Canvas element not found") -// val three: ThreePlugin = props.context.plugins.fetch(ThreePlugin) -// canvas = three.output(element, props.options ?: Canvas3DOptions.empty()).apply { -// onClick = props.clickCallback -// } -// props.canvasCallback?.invoke(canvas) -// } -// canvas?.render(obj) -// } -// } -// -// override fun componentDidUpdate(prevProps: ThreeCanvasProps, prevState: ThreeCanvasState, snapshot: Any) { -// if (prevProps.obj != props.obj) { -// componentDidMount() -// } -// if (prevProps.selected != props.selected) { -// canvas?.select(props.selected) -// } -// } -// -// override fun RBuilder.render() { -// div { -// ref { -// state.element = findDOMNode(it) -// } -// } -// } -//} \ No newline at end of file +} \ No newline at end of file diff --git a/ui/react/src/main/kotlin/hep/dataforge/vision/react/TreeStyles.kt b/ui/react/src/main/kotlin/hep/dataforge/vision/react/TreeStyles.kt index 7cfb7d48..eb0bb8b9 100644 --- a/ui/react/src/main/kotlin/hep/dataforge/vision/react/TreeStyles.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vision/react/TreeStyles.kt @@ -8,7 +8,7 @@ public object TreeStyles : StyleSheet("treeStyles", true) { /** * Remove default bullets */ - public val tree by css { + public val tree: RuleSet by css { paddingLeft = 8.px marginLeft = 0.px listStyleType = ListStyleType.none diff --git a/visionforge-core/build.gradle.kts b/visionforge-core/build.gradle.kts index b878791b..6eee3f0c 100644 --- a/visionforge-core/build.gradle.kts +++ b/visionforge-core/build.gradle.kts @@ -28,8 +28,8 @@ kotlin { jsMain { dependencies { api("hep.dataforge:dataforge-output-html:$dataforgeVersion") - api("org.jetbrains:kotlin-styled:5.2.0-$kotlinWrappersVersion") api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion") + api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion") } } } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt index 0ef38797..d39daaf0 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/specifications/Canvas3DOptions.kt @@ -7,7 +7,7 @@ public class Canvas3DOptions : Scheme() { public var camera: Camera by spec(Camera, Camera.empty()) public var controls: Controls by spec(Controls, Controls.empty()) - public var minSize: Int by int(300) + public var minSize: Int by int(400) public var minWith: Number by number { minSize } public var minHeight: Number by number { minSize } @@ -17,4 +17,10 @@ public class Canvas3DOptions : Scheme() { public companion object : SchemeSpec(::Canvas3DOptions) -} \ No newline at end of file +} + +public fun Canvas3DOptions.computeWidth(external: Number): Int = + (external.toInt()).coerceIn(minWith.toInt()..maxWith.toInt()) + +public fun Canvas3DOptions.computeHeight(external: Number): Int = + (external.toInt()).coerceIn(minHeight.toInt()..maxHeight.toInt()) \ No newline at end of file diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/DatCanvasControls.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/DatCanvasControls.kt new file mode 100644 index 00000000..361e18e3 --- /dev/null +++ b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/DatCanvasControls.kt @@ -0,0 +1,5 @@ +package hep.dataforge.vision.solid.three + +fun createDataControls() { + val dat = kotlinext.js.require("dat.gui") +} \ No newline at end of file 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 f7243194..33a86ebf 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 @@ -10,9 +10,7 @@ import hep.dataforge.names.toName import hep.dataforge.output.Renderer import hep.dataforge.vision.Colors import hep.dataforge.vision.solid.Solid -import hep.dataforge.vision.solid.specifications.Camera -import hep.dataforge.vision.solid.specifications.Canvas3DOptions -import hep.dataforge.vision.solid.specifications.Controls +import hep.dataforge.vision.solid.specifications.* import hep.dataforge.vision.solid.three.ThreeMaterials.HIGHLIGHT_MATERIAL import hep.dataforge.vision.solid.three.ThreeMaterials.SELECTED_MATERIAL import info.laht.threekt.WebGLRenderer @@ -28,9 +26,11 @@ 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.HTMLCanvasElement import org.w3c.dom.HTMLElement import org.w3c.dom.Node import org.w3c.dom.events.MouseEvent @@ -41,7 +41,6 @@ import kotlin.math.sin * */ public class ThreeCanvas( - element: HTMLElement, public val three: ThreePlugin, public val options: Canvas3DOptions, public val onClick: ((Name?) -> Unit)? = null, @@ -69,7 +68,24 @@ public class ThreeCanvas( private var picked: Object3D? = null - init { + /** + * 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 @@ -86,12 +102,18 @@ public class ThreeCanvas( onClick?.invoke(picked?.fullName()) }, false) - camera.aspect = 1.0 - val renderer = WebGLRenderer { antialias = true }.apply { setClearColor(Colors.skyblue, 1) } + val canvas = renderer.domElement as HTMLCanvasElement + + canvas.style.apply { + width = "100%" + height = "100%" + display = "block" + } + addControls(renderer.domElement, options.controls) fun animate() { @@ -106,19 +128,15 @@ public class ThreeCanvas( window.requestAnimationFrame { animate() } + renderer.render(scene, camera) } element.appendChild(renderer.domElement) + renderer.resize() - renderer.setSize( - element.clientWidth.coerceIn(options.minWith.toInt()..options.maxWith.toInt()), - element.clientHeight.coerceIn(options.minHeight.toInt()..options.maxHeight.toInt()) - ) - - window.onresize = { - renderer.setSize(element.clientWidth, element.clientWidth) - camera.updateProjectionMatrix() + element.onresize = { + renderer.resize() } animate() @@ -250,7 +268,7 @@ public fun ThreePlugin.output( element: HTMLElement, spec: Canvas3DOptions = Canvas3DOptions.empty(), onClick: ((Name?) -> Unit)? = null, -): ThreeCanvas = ThreeCanvas(element, this, spec, onClick) +): ThreeCanvas = ThreeCanvas(this, spec, onClick).apply { attach(element) } public fun ThreePlugin.render( element: HTMLElement, diff --git a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/three.kt b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/three.kt index fc497c11..2b35b7e7 100644 --- a/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/three.kt +++ b/visionforge-solid/src/jsMain/kotlin/hep/dataforge/vision/solid/three/three.kt @@ -5,10 +5,7 @@ import hep.dataforge.meta.float import hep.dataforge.meta.get import hep.dataforge.meta.node import hep.dataforge.vision.solid.* -import info.laht.threekt.core.BufferGeometry -import info.laht.threekt.core.DirectGeometry -import info.laht.threekt.core.Face3 -import info.laht.threekt.core.Geometry +import info.laht.threekt.core.* import info.laht.threekt.external.controls.OrbitControls import info.laht.threekt.materials.Material import info.laht.threekt.math.Euler @@ -76,4 +73,6 @@ internal fun Any.dispose() { is OrbitControls -> dispose() is Texture -> dispose() } -} \ No newline at end of file +} + +public fun Layers.check(layer: Int): Boolean = (mask shr(layer) and 0x00000001) > 0 \ No newline at end of file