controls redesign

This commit is contained in:
Alexander Nozik 2020-11-09 19:51:57 +03:00
parent 5b8d298ac6
commit fdc221dfa1
19 changed files with 687 additions and 502 deletions

View File

@ -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<GDMLAppProps>("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<GDMLAppProps>("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<GDMLAppProps>("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)
}
}
}

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Three js demo for particle physics</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/main.css">

View File

@ -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")
}
}
}

View File

@ -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<MMAppProps>("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 {

View File

@ -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<String, Any>) {
val context = Global.context("demo") {}

View File

@ -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

View File

@ -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<HTMLElement>.card(title: String, crossinline block: TagConsumer<HTMLElement>.() -> Unit) {
// div("card w-100") {
// div("card-body") {
// h3(classes = "card-title") { +title }
// block()
// }
// }
//}
public inline fun TagConsumer<HTMLElement>.card(title: String, crossinline block: TagConsumer<HTMLElement>.() -> 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<HTMLElement>.accordion(id: String, elements: List<Pair<String, DIV.() -> 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<Pair<String, DIV.() -> Unit>>
public fun AccordionBuilder.entry(title: String, builder: DIV.() -> Unit) {
add(title to builder)
}
public fun TagConsumer<HTMLElement>.accordion(id: String, builder: AccordionBuilder.() -> Unit) {
val list = ArrayList<Pair<String, DIV.() -> Unit>>().apply(builder)
accordion(id, list)
}
public fun RBuilder.accordion(id: String, elements: List<Pair<String, RDOMBuilder<DIV>.() -> 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<NameToken>(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<Pair<String, RDOMBuilder<DIV>.() -> Unit>>
public fun RAccordionBuilder.entry(title: String, builder: RDOMBuilder<DIV>.() -> Unit) {
add(title to builder)
}
public fun RBuilder.accordion(id: String, builder: RAccordionBuilder.() -> Unit): ReactElement {
val list = ArrayList<Pair<String, RDOMBuilder<DIV>.() -> 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<DIV>.() -> Unit
): ReactElement = styledDiv{
css{
classes.add("container${size.suffix}")
}
block()
}
//public typealias SectionsBuilder = MutableList<Pair<String, DIV.() -> 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<HTMLElement>.accordion(id: String, elements: List<Pair<String, DIV.() -> 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<DIV>.() -> Unit
): ReactElement = styledDiv {
val weightSuffix = weight?.let { "-$it" } ?: ""
css {
classes.add("col${maxSize.suffix}$weightSuffix")
}
block()
}
public inline fun RBuilder.gridRow(
block: StyledDOMBuilder<DIV>.() -> Unit
): ReactElement = styledDiv{
css{
classes.add("row")
}
block()
}
//public fun TagConsumer<HTMLElement>.accordion(id: String, builder: AccordionBuilder.() -> Unit) {
// val list = ArrayList<Pair<String, DIV.() -> 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
}
}
}
}
//public fun Element.displayCanvasControls(canvas: ThreeCanvas, block: TagConsumer<HTMLElement>.() -> 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()
// }
//}

View File

@ -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,62 +43,75 @@ public external interface CanvasControlsProps : RProps {
public var canvas: ThreeCanvas
}
public val CanvasControls: FunctionalComponent<CanvasControlsProps> = functionalComponent ("CanvasControls") { props ->
public val CanvasControls: FunctionalComponent<CanvasControlsProps> = 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
}
}
}
}
}
}
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)
}
onChangeFunction = {
if ((it.target as HTMLInputElement).checked) {
props.canvas.camera.layers.enable(layer)
} else {
props.canvas.camera.layers.disable(layer)
}
}
}
@ -106,65 +121,3 @@ public val CanvasControls: FunctionalComponent<CanvasControlsProps> = functional
}
}
}
public fun Element.displayCanvasControls(canvas: ThreeCanvas, block: TagConsumer<HTMLElement>.() -> 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()
}
}

View File

@ -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<DIV>.() -> 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<Pair<String, StyledDOMBuilder<DIV>.() -> 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<NameToken>(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<Pair<String, StyledDOMBuilder<DIV>.() -> Unit>>
public fun RSectionsBuilder.entry(title: String, builder: StyledDOMBuilder<DIV>.() -> Unit) {
add(title to builder)
}
public fun RBuilder.accordion(id: String, builder: RSectionsBuilder.() -> Unit): ReactElement {
val list = ArrayList<Pair<String, StyledDOMBuilder<DIV>.() -> 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<DIV>.() -> 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<DIV>.() -> Unit,
): ReactElement = styledDiv {
val weightSuffix = weight?.let { "-$it" } ?: ""
css {
classes.add("col${maxSize.suffix}$weightSuffix")
}
block()
}
public inline fun RBuilder.gridRow(
block: StyledDOMBuilder<DIV>.() -> Unit,
): ReactElement = styledDiv {
css {
classes.add("row")
}
block()
}

View File

@ -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<TabProps> = functionalComponent { props ->
props.children()
}
public external class TabPaneProps : RProps {
public var activeTab: String?
}
@JsExport
public val TabPane: FunctionalComponent<TabPaneProps> = functionalComponent("TabPane") { props ->
var activeTab: String? by useState(props.activeTab)
val children: Array<out ReactElement?> = Children.map(props.children) {
it.asElementOrNull()
} ?: emptyArray()
val childrenProps = children.mapNotNull {
it?.props?.unsafeCast<TabProps>()
}
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<TabProps>())?.id == activeTab }?.let {
child(it)
}
}
}
public class TabBuilder(internal val parentBuilder: RBuilder) {
public fun tab(id: String, title: String? = null, builder: StyledDOMBuilder<DIV>.() -> 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()
}
}

View File

@ -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<ThreeControlsProps> = 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()
}

View File

@ -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")
}

View File

@ -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<ThreeCanvasProps> = 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<ThreeCanvasProps> = 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<ThreeCanvasProps, ThreeCanvasState>() {
//
// 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)
// }
// }
// }
//}

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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 }
@ -18,3 +18,9 @@ public class Canvas3DOptions : Scheme() {
public companion object : SchemeSpec<Canvas3DOptions>(::Canvas3DOptions)
}
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())

View File

@ -0,0 +1,5 @@
package hep.dataforge.vision.solid.three
fun createDataControls() {
val dat = kotlinext.js.require("dat.gui")
}

View File

@ -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,

View File

@ -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
@ -77,3 +74,5 @@ internal fun Any.dispose() {
is Texture -> dispose()
}
}
public fun Layers.check(layer: Int): Boolean = (mask shr(layer) and 0x00000001) > 0