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.context.Context
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.isEmpty
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionGroup import hep.dataforge.vision.bootstrap.card
import hep.dataforge.vision.bootstrap.* import hep.dataforge.vision.bootstrap.nameCrumbs
import hep.dataforge.vision.bootstrap.threeControls
import hep.dataforge.vision.gdml.toVision 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.Solid
import hep.dataforge.vision.solid.SolidGroup
import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.SolidManager
import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreeCanvas
import hep.dataforge.vision.solid.three.ThreeCanvasComponent
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.css.FlexBasis import kotlinx.css.*
import kotlinx.css.Overflow import kotlinx.css.properties.border
import kotlinx.css.flex
import kotlinx.css.overflow
import kscience.gdml.GDML import kscience.gdml.GDML
import kscience.gdml.decodeFromString import kscience.gdml.decodeFromString
import org.w3c.files.FileReader import org.w3c.files.FileReader
import org.w3c.files.get import org.w3c.files.get
import react.* import react.RProps
import react.child
import react.dom.h1 import react.dom.h1
import react.functionalComponent
import react.useState
import styled.css import styled.css
import styled.styledDiv
external interface GDMLAppProps : RProps { external interface GDMLAppProps : RProps {
var context: Context var context: Context
@ -46,7 +48,7 @@ val GDMLApp = functionalComponent<GDMLAppProps>("GDMLApp") { props ->
var canvas: ThreeCanvas? by useState { null } var canvas: ThreeCanvas? by useState { null }
var vision: Vision? by useState { props.rootObject } var vision: Vision? by useState { props.rootObject }
val select: (Name?) -> Unit = { val onSelect: (Name?) -> Unit = {
selected = it selected = it
} }
@ -67,20 +69,56 @@ val GDMLApp = functionalComponent<GDMLAppProps>("GDMLApp") { props ->
vision = parsedVision vision = parsedVision
} }
gridColumn { flexColumn {
css { css {
flex(1.0, 1.0, FlexBasis.auto) height = 100.pct
width = 100.pct
} }
h1 { +"GDML/JSON loader demo" } styledDiv {
gridRow {
css { css {
+"p-1" +"page-header"
overflow = Overflow.auto +"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 { css {
+"order-2" flex(1.0, 1.0, FlexBasis.auto)
+"order-xl-1" 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") { card("Load data") {
fileDrop("(drag file here)") { files -> fileDrop("(drag file here)") { files ->
@ -97,61 +135,8 @@ val GDMLApp = functionalComponent<GDMLAppProps>("GDMLApp") { props ->
} }
} }
} }
//tree canvas?.let {
card("Object tree") { threeControls(it, selected, onSelect)
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
)
}
}
} }
} }
} }

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <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> <title>Three js demo for particle physics</title>
<link rel="stylesheet" href="css/bootstrap.min.css"> <link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="css/main.css">

View File

@ -48,7 +48,7 @@ kotlin {
dependencies { dependencies {
implementation(project(":ui:bootstrap")) implementation(project(":ui:bootstrap"))
implementation("io.ktor:ktor-client-js:$ktorVersion") 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.Vision
import hep.dataforge.vision.bootstrap.canvasControls import hep.dataforge.vision.bootstrap.canvasControls
import hep.dataforge.vision.bootstrap.card import hep.dataforge.vision.bootstrap.card
import hep.dataforge.vision.react.ThreeCanvasComponent
import hep.dataforge.vision.react.configEditor import hep.dataforge.vision.react.configEditor
import hep.dataforge.vision.react.objectTree import hep.dataforge.vision.react.objectTree
import hep.dataforge.vision.solid.specifications.Camera import hep.dataforge.vision.solid.specifications.Camera
import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.solid.specifications.Canvas3DOptions
import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreeCanvas
import hep.dataforge.vision.solid.three.ThreeCanvasComponent
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.get import io.ktor.client.request.get
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.css.height
import kotlinx.css.pct
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import react.* import react.RProps
import react.child
import react.dom.* import react.dom.*
import react.functionalComponent
import react.useState
import styled.css import styled.css
import styled.styledDiv import styled.styledDiv
import kotlin.math.PI import kotlin.math.PI
@ -62,13 +67,18 @@ val MMApp = functionalComponent<MMAppProps>("Muon monitor") { props ->
+"col-lg-3" +"col-lg-3"
+"px-0" +"px-0"
+"overflow-auto" +"overflow-auto"
+"h-100"
} }
//tree //tree
card("Object tree") { card("Object tree") {
objectTree(root, selected, select) objectTree(root, selected, select)
} }
} }
div("col-lg-6") { styledDiv {
css {
+"col-lg-6"
height = 100.pct
}
//canvas //canvas
child(ThreeCanvasComponent) { child(ThreeCanvasComponent) {
attrs { attrs {

View File

@ -3,12 +3,10 @@ package ru.mipt.npm.muon.monitor
import hep.dataforge.context.Global import hep.dataforge.context.Global
import hep.dataforge.js.Application import hep.dataforge.js.Application
import hep.dataforge.js.startApplication import hep.dataforge.js.startApplication
import hep.dataforge.vision.solid.SolidManager
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer import io.ktor.client.features.json.serializer.KotlinxSerializer
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.serialization.json.Json
import react.child import react.child
import react.dom.div import react.dom.div
import react.dom.render import react.dom.render
@ -19,12 +17,10 @@ private class MMDemoApp : Application {
private val connection = HttpClient { private val connection = HttpClient {
install(JsonFeature) { install(JsonFeature) {
serializer = KotlinxSerializer(Json { serializersModule = SolidManager.serializersModuleForSolids }) serializer = KotlinxSerializer()
} }
} }
//TODO introduce react application
override fun start(state: Map<String, Any>) { override fun start(state: Map<String, Any>) {
val context = Global.context("demo") {} 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.Application
import hep.dataforge.js.startApplication import hep.dataforge.js.startApplication
import hep.dataforge.vision.bootstrap.visionPropertyEditor import hep.dataforge.vision.bootstrap.visionPropertyEditor
import hep.dataforge.vision.react.ThreeCanvasComponent
import hep.dataforge.vision.react.objectTree import hep.dataforge.vision.react.objectTree
import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.*
import hep.dataforge.vision.solid.specifications.Canvas3DOptions import hep.dataforge.vision.solid.specifications.Canvas3DOptions
import hep.dataforge.vision.solid.three.ThreeCanvasComponent
import hep.dataforge.vision.solid.three.ThreePlugin import hep.dataforge.vision.solid.three.ThreePlugin
import kotlinx.browser.document import kotlinx.browser.document
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement

View File

@ -1,238 +1,116 @@
package hep.dataforge.vision.bootstrap package hep.dataforge.vision.bootstrap
import hep.dataforge.names.Name //public inline fun TagConsumer<HTMLElement>.card(title: String, crossinline block: TagConsumer<HTMLElement>.() -> Unit) {
import hep.dataforge.names.NameToken // div("card w-100") {
import hep.dataforge.names.length // div("card-body") {
import hep.dataforge.vision.Vision // h3(classes = "card-title") { +title }
import hep.dataforge.vision.react.ObjectTree // block()
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) { //public typealias SectionsBuilder = MutableList<Pair<String, DIV.() -> Unit>>
div("card w-100") { //
div("card-body") { //public fun SectionsBuilder.entry(title: String, builder: DIV.() -> Unit) {
h3(classes = "card-title") { +title } // add(title to builder)
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 enum class GridMaxSize(public val suffix: String) { //public fun TagConsumer<HTMLElement>.accordion(id: String, elements: List<Pair<String, DIV.() -> Unit>>) {
NONE(""), // div("container-fluid") {
SM("-sm"), // div("accordion") {
MD("-md"), // this.id = id
LG("-lg"), // elements.forEachIndexed { index, (title, builder) ->
XL("-xl") // 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( //public fun TagConsumer<HTMLElement>.accordion(id: String, builder: AccordionBuilder.() -> Unit) {
block: StyledDOMBuilder<DIV>.() -> Unit // val list = ArrayList<Pair<String, DIV.() -> Unit>>().apply(builder)
): ReactElement = styledDiv{ // accordion(id, list)
css{ //}
classes.add("row")
}
block()
}
public fun Element.renderObjectTree( //public fun Element.displayCanvasControls(canvas: ThreeCanvas, block: TagConsumer<HTMLElement>.() -> Unit = {}) {
vision: Vision, // clear()
clickCallback: (Name) -> Unit = {} // append {
): Unit = render(this) { // accordion("controls") {
card("Object tree") { // entry("Settings") {
child(ObjectTree) { // div("row") {
attrs { // div("col-2") {
this.name = Name.EMPTY // label("checkbox-inline") {
this.obj = vision // input(type = InputType.checkBox) {
this.selected = null // checked = canvas.axes.visible
this.clickCallback = clickCallback // 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 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.SolidGroup
import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.SolidManager
import hep.dataforge.vision.solid.three.ThreeCanvas import hep.dataforge.vision.solid.three.ThreeCanvas
import kotlinx.dom.clear import kotlinx.css.*
import kotlinx.html.* import kotlinx.css.properties.border
import kotlinx.html.dom.append import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
import org.w3c.files.Blob import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag import org.w3c.files.BlobPropertyBag
import react.* import react.*
import react.dom.button import react.dom.button
import react.dom.div import react.dom.h3
import react.dom.input import react.dom.input
import react.dom.label import react.dom.label
import styled.css
import styled.styledDiv
private fun saveData(event: Event, fileName: String, mimeType: String = "text/plain", dataBuilder: () -> String) { private fun saveData(event: Event, fileName: String, mimeType: String = "text/plain", dataBuilder: () -> String) {
event.stopPropagation(); event.stopPropagation();
@ -30,8 +32,8 @@ private fun saveData(event: Event, fileName: String, mimeType: String = "text/pl
} }
public fun RBuilder.canvasControls(canvas: ThreeCanvas): ReactElement { public fun RBuilder.canvasControls(canvas: ThreeCanvas): ReactElement {
return child(CanvasControls){ return child(CanvasControls) {
attrs{ attrs {
this.canvas = canvas this.canvas = canvas
} }
} }
@ -41,62 +43,75 @@ public external interface CanvasControlsProps : RProps {
public var canvas: ThreeCanvas public var canvas: ThreeCanvas
} }
public val CanvasControls: FunctionalComponent<CanvasControlsProps> = functionalComponent ("CanvasControls") { props -> public val CanvasControls: FunctionalComponent<CanvasControlsProps> = functionalComponent("CanvasControls") { props ->
val visionManager = useMemo( val visionManager = useMemo(
{ props.canvas.context.plugins.fetch(SolidManager).visionManager }, { props.canvas.context.plugins.fetch(SolidManager).visionManager },
arrayOf(props.canvas) arrayOf(props.canvas)
) )
accordion("controls") { flexColumn {
entry("Settings") { h3 { +"Axes" }
div("row") { flexRow {
div("col-2") { css{
label("checkbox-inline") { border(1.px,BorderStyle.solid, Color.blue)
input(type = InputType.checkBox) { padding(4.px)
attrs { }
defaultChecked = props.canvas.axes.visible label("checkbox-inline") {
onChangeFunction = { input(type = InputType.checkBox) {
props.canvas.axes.visible = (it.target as HTMLInputElement).checked attrs {
} defaultChecked = props.canvas.axes.visible
} onChangeFunction = {
props.canvas.axes.visible = (it.target as HTMLInputElement).checked
} }
+"Axes"
} }
} }
div("col-1") { +"Axes"
button { }
+"Export" }
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 { attrs {
onClickFunction = { if (layer == 0) {
val json = (props.canvas.content as? SolidGroup)?.let { group -> defaultChecked = true
visionManager.encodeToString(group)
}
if (json != null) {
saveData(it, "object.json", "text/json") {
json
}
}
} }
} onChangeFunction = {
} if ((it.target as HTMLInputElement).checked) {
} props.canvas.camera.layers.enable(layer)
} } else {
} props.canvas.camera.layers.disable(layer)
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)
}
} }
} }
} }
@ -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 val kotlinWrappersVersion: String by rootProject.extra
dependencies{ 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") 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.context.Context
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.Solid
import hep.dataforge.vision.solid.specifications.Canvas3DOptions 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.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import react.* import react.*
import react.dom.div import styled.css
import styled.styledDiv
public external interface ThreeCanvasProps : RProps { public external interface ThreeCanvasProps : RProps {
public var context: Context public var context: Context
@ -33,7 +38,8 @@ public val ThreeCanvasComponent: FunctionalComponent<ThreeCanvasProps> = functio
if (canvas == null) { if (canvas == null) {
val element = elementRef.current as? HTMLElement ?: error("Canvas element not found") val element = elementRef.current as? HTMLElement ?: error("Canvas element not found")
val three: ThreePlugin = props.context.plugins.fetch(ThreePlugin) 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) props.canvasCallback?.invoke(newCanvas)
canvas = newCanvas canvas = newCanvas
} }
@ -51,43 +57,13 @@ public val ThreeCanvasComponent: FunctionalComponent<ThreeCanvasProps> = functio
canvas?.select(props.selected) 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 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 * Remove default bullets
*/ */
public val tree by css { public val tree: RuleSet by css {
paddingLeft = 8.px paddingLeft = 8.px
marginLeft = 0.px marginLeft = 0.px
listStyleType = ListStyleType.none listStyleType = ListStyleType.none

View File

@ -28,8 +28,8 @@ kotlin {
jsMain { jsMain {
dependencies { dependencies {
api("hep.dataforge:dataforge-output-html:$dataforgeVersion") 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-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 camera: Camera by spec(Camera, Camera.empty())
public var controls: Controls by spec(Controls, Controls.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 minWith: Number by number { minSize }
public var minHeight: 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 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.output.Renderer
import hep.dataforge.vision.Colors import hep.dataforge.vision.Colors
import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.Solid
import hep.dataforge.vision.solid.specifications.Camera import hep.dataforge.vision.solid.specifications.*
import hep.dataforge.vision.solid.specifications.Canvas3DOptions
import hep.dataforge.vision.solid.specifications.Controls
import hep.dataforge.vision.solid.three.ThreeMaterials.HIGHLIGHT_MATERIAL import hep.dataforge.vision.solid.three.ThreeMaterials.HIGHLIGHT_MATERIAL
import hep.dataforge.vision.solid.three.ThreeMaterials.SELECTED_MATERIAL import hep.dataforge.vision.solid.three.ThreeMaterials.SELECTED_MATERIAL
import info.laht.threekt.WebGLRenderer 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.math.Vector2
import info.laht.threekt.objects.LineSegments import info.laht.threekt.objects.LineSegments
import info.laht.threekt.objects.Mesh import info.laht.threekt.objects.Mesh
import info.laht.threekt.renderers.WebGLRenderer
import info.laht.threekt.scenes.Scene import info.laht.threekt.scenes.Scene
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.dom.clear import kotlinx.dom.clear
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.events.MouseEvent import org.w3c.dom.events.MouseEvent
@ -41,7 +41,6 @@ import kotlin.math.sin
* *
*/ */
public class ThreeCanvas( public class ThreeCanvas(
element: HTMLElement,
public val three: ThreePlugin, public val three: ThreePlugin,
public val options: Canvas3DOptions, public val options: Canvas3DOptions,
public val onClick: ((Name?) -> Unit)? = null, public val onClick: ((Name?) -> Unit)? = null,
@ -69,7 +68,24 @@ public class ThreeCanvas(
private var picked: Object3D? = null 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() element.clear()
//Attach listener to track mouse changes //Attach listener to track mouse changes
@ -86,12 +102,18 @@ public class ThreeCanvas(
onClick?.invoke(picked?.fullName()) onClick?.invoke(picked?.fullName())
}, false) }, false)
camera.aspect = 1.0
val renderer = WebGLRenderer { antialias = true }.apply { val renderer = WebGLRenderer { antialias = true }.apply {
setClearColor(Colors.skyblue, 1) setClearColor(Colors.skyblue, 1)
} }
val canvas = renderer.domElement as HTMLCanvasElement
canvas.style.apply {
width = "100%"
height = "100%"
display = "block"
}
addControls(renderer.domElement, options.controls) addControls(renderer.domElement, options.controls)
fun animate() { fun animate() {
@ -106,19 +128,15 @@ public class ThreeCanvas(
window.requestAnimationFrame { window.requestAnimationFrame {
animate() animate()
} }
renderer.render(scene, camera) renderer.render(scene, camera)
} }
element.appendChild(renderer.domElement) element.appendChild(renderer.domElement)
renderer.resize()
renderer.setSize( element.onresize = {
element.clientWidth.coerceIn(options.minWith.toInt()..options.maxWith.toInt()), renderer.resize()
element.clientHeight.coerceIn(options.minHeight.toInt()..options.maxHeight.toInt())
)
window.onresize = {
renderer.setSize(element.clientWidth, element.clientWidth)
camera.updateProjectionMatrix()
} }
animate() animate()
@ -250,7 +268,7 @@ public fun ThreePlugin.output(
element: HTMLElement, element: HTMLElement,
spec: Canvas3DOptions = Canvas3DOptions.empty(), spec: Canvas3DOptions = Canvas3DOptions.empty(),
onClick: ((Name?) -> Unit)? = null, onClick: ((Name?) -> Unit)? = null,
): ThreeCanvas = ThreeCanvas(element, this, spec, onClick) ): ThreeCanvas = ThreeCanvas(this, spec, onClick).apply { attach(element) }
public fun ThreePlugin.render( public fun ThreePlugin.render(
element: HTMLElement, element: HTMLElement,

View File

@ -5,10 +5,7 @@ import hep.dataforge.meta.float
import hep.dataforge.meta.get import hep.dataforge.meta.get
import hep.dataforge.meta.node import hep.dataforge.meta.node
import hep.dataforge.vision.solid.* import hep.dataforge.vision.solid.*
import info.laht.threekt.core.BufferGeometry import info.laht.threekt.core.*
import info.laht.threekt.core.DirectGeometry
import info.laht.threekt.core.Face3
import info.laht.threekt.core.Geometry
import info.laht.threekt.external.controls.OrbitControls import info.laht.threekt.external.controls.OrbitControls
import info.laht.threekt.materials.Material import info.laht.threekt.materials.Material
import info.laht.threekt.math.Euler import info.laht.threekt.math.Euler
@ -77,3 +74,5 @@ internal fun Any.dispose() {
is Texture -> dispose() is Texture -> dispose()
} }
} }
public fun Layers.check(layer: Int): Boolean = (mask shr(layer) and 0x00000001) > 0