Compare commits

...

2 Commits

Author SHA1 Message Date
da0f4c0ff0 Optimize UI 2023-12-31 18:09:36 +03:00
7871987df1 Optimize UI 2023-12-28 10:20:02 +03:00
22 changed files with 186 additions and 174 deletions

View File

@ -1,27 +1,36 @@
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose)
} }
group = "demo" group = "demo"
kscience { kscience {
jvm() // jvm()
js { js {
browser { browser {
binaries.executable() binaries.executable()
commonWebpackConfig{
cssSupport{
enabled = true
}
scssSupport{
enabled = true
}
sourceMaps = true
}
} }
} }
dependencies { dependencies {
implementation(projects.visionforgeSolid) implementation(projects.visionforgeSolid)
implementation(projects.visionforgeGdml) implementation(projects.visionforgeGdml)
} }
jvmMain { // jvmMain {
// implementation(project(":visionforge-fx")) //// implementation(project(":visionforge-fx"))
implementation(spclibs.logback.classic) // implementation(spclibs.logback.classic)
} // }
jsMain { jsMain {
implementation(projects.visionforgeThreejs) implementation(projects.visionforgeThreejs)
implementation(npm("react-file-drop", "3.0.6"))
} }
} }

View File

@ -3,12 +3,12 @@
package space.kscience.visionforge.gdml.demo package space.kscience.visionforge.gdml.demo
import androidx.compose.runtime.* import androidx.compose.runtime.*
import app.softwork.bootstrapcompose.Container
import app.softwork.bootstrapcompose.Icon
import org.jetbrains.compose.web.ExperimentalComposeWebApi import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.attributes.InputType import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.name import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.I
import org.jetbrains.compose.web.dom.Input import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.dom.Text
import org.w3c.files.FileList import org.w3c.files.FileList
@ -22,7 +22,7 @@ fun FileDrop(
) { ) {
var dragOver by remember { mutableStateOf(false) } var dragOver by remember { mutableStateOf(false) }
Div({ Container(attrs = {
id("dropzone") id("dropzone")
style { style {
border( border(
@ -69,8 +69,7 @@ fun FileDrop(
} }
} }
}) { }) {
Icon("cloud-upload"){ classes("dropzone-icon") }
I({ classes("bi", "bi-cloud-upload", "dropzone-icon") })
Text(title) Text(title)
Input(type = InputType.File, attrs = { Input(type = InputType.File, attrs = {
style { style {

View File

@ -4,7 +4,7 @@ import androidx.compose.runtime.*
import kotlinx.browser.window import kotlinx.browser.window
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.H2 import org.jetbrains.compose.web.dom.P
import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.dom.Text
import org.w3c.files.File import org.w3c.files.File
import org.w3c.files.FileReader import org.w3c.files.FileReader
@ -66,7 +66,7 @@ fun GDMLApp(solids: Solids, initialVision: Solid?, selected: Name? = null) {
}) { }) {
ThreeView(solids, vision, selected) { ThreeView(solids, vision, selected) {
Tab("Load") { Tab("Load") {
H2 { P {
Text("Drag and drop .gdml or .json VisionForge files here") Text("Drag and drop .gdml or .json VisionForge files here")
} }
FileDrop("(drag file here)") { files -> FileDrop("(drag file here)") { files ->

View File

@ -47,8 +47,7 @@ private class JsPlaygroundApp : Application {
width(100.vw) width(100.vw)
} }
}) { }) {
Tabs { Tabs("gravity") {
active = "gravity"
Tab("gravity") { Tab("gravity") {
GravityDemo(solids, client) GravityDemo(solids, client)
} }

View File

@ -5,7 +5,9 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import app.softwork.bootstrapcompose.Button import app.softwork.bootstrapcompose.Button
import app.softwork.bootstrapcompose.ButtonGroup import app.softwork.bootstrapcompose.ButtonGroup
import app.softwork.bootstrapcompose.Color.Secondary
import app.softwork.bootstrapcompose.Container import app.softwork.bootstrapcompose.Container
import app.softwork.bootstrapcompose.Layout.Width
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.await import kotlinx.coroutines.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,7 +66,7 @@ fun MMApp(solids: Solids, model: Model, selected: Name? = null) {
options = mmOptions, options = mmOptions,
sidebarTabs = { sidebarTabs = {
Tab("Events") { Tab("Events") {
ButtonGroup { ButtonGroup({ Layout.width = Width.Full }) {
Button("Next") { Button("Next") {
solids.context.launch { solids.context.launch {
val event = window.fetch( val event = window.fetch(
@ -83,7 +85,7 @@ fun MMApp(solids: Solids, model: Model, selected: Name? = null) {
model.displayEvent(event) model.displayEvent(event)
} }
} }
Button("Clear") { Button("Clear", color = Secondary) {
events.clear() events.clear()
model.reset() model.reset()
} }

View File

@ -14,10 +14,12 @@ import space.kscience.dataforge.meta.Null
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Colors import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.meta
import space.kscience.visionforge.server.close import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.visionPage import space.kscience.visionforge.server.visionPage
import space.kscience.visionforge.solid.* import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
import space.kscience.visionforge.three.threeJsHeader import space.kscience.visionforge.three.threeJsHeader
import kotlin.random.Random import kotlin.random.Random
@ -47,7 +49,12 @@ fun main() {
) { ) {
div("flex-column") { div("flex-column") {
h1 { +"Satellite detector demo" } h1 { +"Satellite detector demo" }
vision { sat } vision {
meta(Canvas3DOptions {
controls.enabled = false
})
sat
}
} }
} }

View File

@ -46,7 +46,7 @@ internal class VariableBox(val xSize: Number, val ySize: Number) : ThreeJsVision
mesh.scale.z = properties.getValue(VALUE)?.number?.toDouble() ?: 1.0 mesh.scale.z = properties.getValue(VALUE)?.number?.toDouble() ?: 1.0
//add listener to object properties //add listener to object properties
onPropertyChange { name -> onPropertyChange(three.context) { name ->
when { when {
name == VALUE -> { name == VALUE -> {
val value = properties.getValue(VALUE)?.int ?: 0 val value = properties.getValue(VALUE)?.int ?: 0

View File

@ -0,0 +1,28 @@
package space.kscience.visionforge.compose
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.css.Style
import org.jetbrains.compose.web.dom.DOMScope
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.Element
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionClient
/**
* An [ElementVisionRenderer] that could be used directly in Compose-html as well as a stand-alone renderer
*/
public interface ComposeVisionRenderer: ElementVisionRenderer {
@Composable
public fun DOMScope<Element>.render(client: VisionClient, name: Name, vision: Vision, meta: Meta)
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
renderComposable(element) {
Style(VisionForgeStyles)
render(client, name, vision, meta)
}
}
}

View File

@ -1,10 +1,7 @@
package space.kscience.visionforge.compose package space.kscience.visionforge.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.Li import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.Nav
import org.jetbrains.compose.web.dom.Ol
import org.jetbrains.compose.web.dom.Text
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.length import space.kscience.dataforge.names.length
@ -17,7 +14,7 @@ public fun NameCrumbs(name: Name?, link: (Name) -> Unit): Unit = Nav({
classes("breadcrumb") classes("breadcrumb")
style { style {
property("--bs-breadcrumb-divider", "'.'") property("--bs-breadcrumb-divider", "'.'")
property("--bs-breadcrumb-item-padding-x",".1rem") property("--bs-breadcrumb-item-padding-x", ".1rem")
} }
}) { }) {
Li({ Li({
@ -26,7 +23,9 @@ public fun NameCrumbs(name: Name?, link: (Name) -> Unit): Unit = Nav({
link(Name.EMPTY) link(Name.EMPTY)
} }
}) { }) {
Text("\u2302") A("#") {
Text("\u2302")
}
} }
if (name != null) { if (name != null) {
@ -41,7 +40,9 @@ public fun NameCrumbs(name: Name?, link: (Name) -> Unit): Unit = Nav({
link(fullName) link(fullName)
} }
}) { }) {
Text(token.toString()) A("#") {
Text(token.toString())
}
} }
} }
} }

View File

@ -28,9 +28,9 @@ import space.kscience.visionforge.hidden
* The display state of a property * The display state of a property
*/ */
public sealed class EditorPropertyState { public sealed class EditorPropertyState {
public object Defined : EditorPropertyState() public data object Defined : EditorPropertyState()
public class Default(public val source: String = "unknown") : EditorPropertyState() public data class Default(public val source: String = "unknown") : EditorPropertyState()
public object Undefined : EditorPropertyState() public data object Undefined : EditorPropertyState()
} }

View File

@ -69,7 +69,7 @@ public class TabsBuilder {
@Composable @Composable
public fun Tab( public fun Tab(
key: String, key: String,
label: ContentBuilder<HTMLAnchorElement> = { Text(key) }, label: ContentBuilder<HTMLAnchorElement> = { A("#") { Text(key) } },
disabled: Boolean = false, disabled: Boolean = false,
content: ContentBuilder<HTMLDivElement>, content: ContentBuilder<HTMLDivElement>,
) { ) {

View File

@ -10,7 +10,7 @@ public object TreeStyles : StyleSheet(VisionForgeStyles) {
* Remove default bullets * Remove default bullets
*/ */
public val tree: String by style { public val tree: String by style {
paddingLeft(5.px) paddingLeft(10.px)
marginLeft(0.px) marginLeft(0.px)
listStyleType("none") listStyleType("none")
} }

View File

@ -3,16 +3,14 @@
package space.kscience.visionforge.compose package space.kscience.visionforge.compose
import androidx.compose.runtime.* import androidx.compose.runtime.*
import kotlinx.uuid.UUID
import kotlinx.uuid.generateUUID
import org.jetbrains.compose.web.attributes.* import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.dom.Input import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.Option
import org.jetbrains.compose.web.dom.Select
import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.HTMLOptionElement import org.w3c.dom.HTMLOptionElement
import org.w3c.dom.asList import org.w3c.dom.asList
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.ValueRestriction
import space.kscience.dataforge.meta.descriptors.allowedValues import space.kscience.dataforge.meta.descriptors.allowedValues
import space.kscience.visionforge.Colors import space.kscience.visionforge.Colors
import space.kscience.visionforge.widgetType import space.kscience.visionforge.widgetType
@ -47,20 +45,31 @@ public fun BooleanValueChooser(
value: Value?, value: Value?,
onValueChange: (Value?) -> Unit, onValueChange: (Value?) -> Unit,
) { ) {
val uid = remember { "checkbox[${UUID.generateUUID().toString(false)}]" }
var innerValue by remember(value, descriptor) { var innerValue by remember(value, descriptor) {
mutableStateOf( mutableStateOf(
value?.boolean ?: descriptor?.defaultValue?.boolean value?.boolean ?: descriptor?.defaultValue?.boolean
) )
} }
Input(type = InputType.Checkbox) { Input(type = InputType.Checkbox) {
classes("w-100") classes("btn-check")
checked(innerValue ?: false) checked(innerValue ?: false)
autoComplete(AutoComplete.off)
id(uid)
onInput { event -> onInput { event ->
innerValue = event.value innerValue = event.value
onValueChange(event.value.asValue()) onValueChange(event.value.asValue())
} }
} }
Label(uid, attrs = { classes("btn", "btn-sm", "btn-outline-secondary", "w-100") }) {
if (innerValue == true) {
Text("On")
} else {
Text("Off")
}
}
} }
@Composable @Composable
@ -70,7 +79,7 @@ public fun NumberValueChooser(
value: Value?, value: Value?,
onValueChange: (Value?) -> Unit, onValueChange: (Value?) -> Unit,
) { ) {
var innerValue by remember(value, descriptor) { mutableStateOf(value?.number) } var innerValue by remember(value, descriptor) { mutableStateOf(value?.number ?: descriptor?.defaultValue?.number) }
Input(type = InputType.Number) { Input(type = InputType.Number) {
classes("w-100") classes("w-100")
@ -159,6 +168,7 @@ public fun MultiSelectChooser(
onValueChange: (Value?) -> Unit, onValueChange: (Value?) -> Unit,
) { ) {
Select({ Select({
classes("w-100","form-select")
onChange { event -> onChange { event ->
val newSelected = event.target.selectedOptions.asList() val newSelected = event.target.selectedOptions.asList()
.map { (it as HTMLOptionElement).value.asValue() } .map { (it as HTMLOptionElement).value.asValue() }
@ -184,39 +194,18 @@ public fun RangeValueChooser(
value: Value?, value: Value?,
onValueChange: (Value?) -> Unit, onValueChange: (Value?) -> Unit,
) { ) {
var innerValue by remember(value, descriptor) { mutableStateOf(value?.double) } var innerValue by remember(value, descriptor) { mutableStateOf(value?.number ?: descriptor?.defaultValue?.number) }
var rangeDisabled: Boolean by remember { mutableStateOf(state != EditorPropertyState.Defined) }
FlexRow {
if (descriptor?.valueRestriction != ValueRestriction.REQUIRED) {
Input(type = InputType.Checkbox) {
if (!rangeDisabled) defaultChecked()
onChange {
val checkBoxValue = it.target.checked
rangeDisabled = !checkBoxValue
onValueChange(
if (!checkBoxValue) {
null
} else {
innerValue?.asValue()
}
)
}
}
}
}
Input(type = InputType.Range) { Input(type = InputType.Range) {
classes("w-100") classes("w-100", "form-range")
if (rangeDisabled) disabled()
value(innerValue?.toString() ?: "") value(innerValue?.toString() ?: "")
onChange { onInput { event ->
val newValue = it.target.value innerValue = event.value
onValueChange(newValue.toDoubleOrNull()?.asValue()) }
innerValue = newValue.toDoubleOrNull() onChange { event ->
innerValue = event.value
onValueChange(innerValue?.asValue())
} }
descriptor?.attributes?.get("min").string?.let { descriptor?.attributes?.get("min").string?.let {
min(it) min(it)

View File

@ -79,8 +79,8 @@ public var Vision.visible: Boolean?
* Subscribe on property updates. The subscription is bound to the given scope and canceled when the scope is canceled * Subscribe on property updates. The subscription is bound to the given scope and canceled when the scope is canceled
*/ */
public fun Vision.onPropertyChange( public fun Vision.onPropertyChange(
scope: CoroutineScope? = manager?.context, scope: CoroutineScope,
callback: suspend (Name) -> Unit, callback: suspend (Name) -> Unit,
): Job = properties.changes.onEach { ): Job = properties.changes.onEach {
callback(it) callback(it)
}.launchIn(scope ?: error("Orphan Vision can't observe properties")) }.launchIn(scope)

View File

@ -4,10 +4,7 @@ import kotlinx.html.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.isEmpty
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
@ -46,9 +43,14 @@ public class VisionOutput @PublishedApi internal constructor(override val contex
newContext.visionManager newContext.visionManager
} }
public inline fun meta(block: MutableMeta.() -> Unit) { }
this.meta = Meta(block)
} public inline fun VisionOutput.meta(block: MutableMeta.() -> Unit) {
this.meta = Meta(block)
}
public fun VisionOutput.meta(metaRepr: MetaRepr) {
this.meta = metaRepr.toMeta()
} }
/** /**

View File

@ -19,7 +19,7 @@ public fun Vision.useProperty(
propertyName: Name, propertyName: Name,
inherit: Boolean? = null, inherit: Boolean? = null,
includeStyles: Boolean? = null, includeStyles: Boolean? = null,
scope: CoroutineScope? = manager?.context, scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
callback: (Meta) -> Unit, callback: (Meta) -> Unit,
): Job { ): Job {
//Pass initial value. //Pass initial value.
@ -28,14 +28,14 @@ public fun Vision.useProperty(
if (name.startsWith(propertyName)) { if (name.startsWith(propertyName)) {
callback(properties.get(propertyName, inherit, includeStyles)) callback(properties.get(propertyName, inherit, includeStyles))
} }
}.launchIn(scope ?: error("Orphan Vision can't observe properties")) }.launchIn(scope)
} }
public fun Vision.useProperty( public fun Vision.useProperty(
propertyName: String, propertyName: String,
inherit: Boolean? = null, inherit: Boolean? = null,
includeStyles: Boolean? = null, includeStyles: Boolean? = null,
scope: CoroutineScope? = manager?.context, scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
callback: (Meta) -> Unit, callback: (Meta) -> Unit,
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback) ): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)

View File

@ -59,6 +59,8 @@ public class CanvasSize : Scheme() {
} }
public class Canvas3DOptions : Scheme() { public class Canvas3DOptions : Scheme() {
public var canvasName: String by string("vision")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
public var axes: AxesScheme by spec(AxesScheme) public var axes: AxesScheme by spec(AxesScheme)
public var camera: CameraScheme by spec(CameraScheme) public var camera: CameraScheme by spec(CameraScheme)

View File

@ -52,7 +52,7 @@ public class ThreeCompositeFactory(public val three: ThreePlugin) : ThreeFactory
applyProperties(vision) applyProperties(vision)
if (observe) { if (observe) {
vision.onPropertyChange { name -> vision.onPropertyChange(three.context) { name ->
when { when {
//name.startsWith(WIREFRAME_KEY) -> mesh.applyWireFrame(obj) //name.startsWith(WIREFRAME_KEY) -> mesh.applyWireFrame(obj)
name.startsWith(EDGES_KEY) -> applyEdges(vision) name.startsWith(EDGES_KEY) -> applyEdges(vision)

View File

@ -1,14 +1,16 @@
package space.kscience.visionforge.solid.three package space.kscience.visionforge.solid.three
import androidx.compose.runtime.Composable
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.jetbrains.compose.web.renderComposable import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.* import space.kscience.dataforge.names.*
import space.kscience.visionforge.* import space.kscience.visionforge.*
import space.kscience.visionforge.compose.ComposeVisionRenderer
import space.kscience.visionforge.solid.* import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.specifications.Canvas3DOptions import space.kscience.visionforge.solid.specifications.Canvas3DOptions
import space.kscience.visionforge.solid.three.compose.ThreeView import space.kscience.visionforge.solid.three.compose.ThreeView
@ -21,7 +23,7 @@ import three.objects.Group as ThreeGroup
/** /**
* A plugin that handles Three Object3D representation of Visions. * A plugin that handles Three Object3D representation of Visions.
*/ */
public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer { public class ThreePlugin : AbstractPlugin(), ComposeVisionRenderer {
override val tag: PluginTag get() = Companion.tag override val tag: PluginTag get() = Companion.tag
public val solids: Solids by require(Solids) public val solids: Solids by require(Solids)
@ -75,7 +77,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
// disable tracking changes for statics // disable tracking changes for statics
group[token] = object3D group[token] = object3D
} catch (ex: Throwable) { } catch (ex: Throwable) {
logger.error(ex) { "Failed to render $child" } logger.error(ex) { "Failed to render vision with token $token and type ${child::class}" }
} }
} }
} }
@ -115,7 +117,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
val object3D = buildObject3D(child) val object3D = buildObject3D(child)
set(childName, object3D) set(childName, object3D)
} catch (ex: Throwable) { } catch (ex: Throwable) {
logger.error(ex) { "Failed to render $child" } logger.error(ex) { "Failed to render vision with name $childName" }
} }
} }
}.launchIn(context) }.launchIn(context)
@ -185,11 +187,10 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
render(vision) render(vision)
} }
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) { @Composable
override fun DOMScope<Element>.render(client: VisionClient, name: Name, vision: Vision, meta: Meta) {
require(vision is Solid) { "Expected Solid but found ${vision::class}" } require(vision is Solid) { "Expected Solid but found ${vision::class}" }
renderComposable(element) { ThreeView(solids, vision, null, Canvas3DOptions.read(meta))
ThreeView(solids, vision, null, Canvas3DOptions.read(meta))
}
} }
public companion object : PluginFactory<ThreePlugin> { public companion object : PluginFactory<ThreePlugin> {

View File

@ -1,12 +1,12 @@
package space.kscience.visionforge.solid.three.compose package space.kscience.visionforge.solid.three.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import app.softwork.bootstrapcompose.Button
import app.softwork.bootstrapcompose.Color.Info
import app.softwork.bootstrapcompose.Column import app.softwork.bootstrapcompose.Column
import app.softwork.bootstrapcompose.Layout.Height import app.softwork.bootstrapcompose.Layout.Height
import app.softwork.bootstrapcompose.Row import app.softwork.bootstrapcompose.Layout.Width
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.dom.Hr
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Text
import org.w3c.files.Blob import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag import org.w3c.files.BlobPropertyBag
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
@ -22,32 +22,16 @@ internal fun CanvasControls(
options: Canvas3DOptions, options: Canvas3DOptions,
) { ) {
Column { Column {
Row(attrs = { vision?.let { vision ->
style { Button("Export", color = Info, styling = { Layout.width = Width.Full }) {
border { val json = vision.encodeToString()
width(1.px)
style(LineStyle.Solid)
color(Color("blue"))
}
padding(4.px)
}
}) {
vision?.let { vision ->
Button({
onClick { event ->
val json = vision.encodeToString()
event.stopPropagation();
event.preventDefault();
val fileSaver = kotlinext.js.require<dynamic>("file-saver") val fileSaver = kotlinext.js.require<dynamic>("file-saver")
val blob = Blob(arrayOf(json), BlobPropertyBag("text/json;charset=utf-8")) val blob = Blob(arrayOf(json), BlobPropertyBag("text/json;charset=utf-8"))
fileSaver.saveAs(blob, "object.json") as Unit fileSaver.saveAs(blob, "${options.canvasName}.json") as Unit
}
}) {
Text("Export")
}
} }
} }
Hr()
PropertyEditor( PropertyEditor(
scope = vision?.manager?.context ?: Global, scope = vision?.manager?.context ?: Global,
properties = options.meta, properties = options.meta,

View File

@ -7,7 +7,6 @@ import app.softwork.bootstrapcompose.Layout.Height
import app.softwork.bootstrapcompose.Layout.Width import app.softwork.bootstrapcompose.Layout.Width
import app.softwork.bootstrapcompose.Row import app.softwork.bootstrapcompose.Row
import kotlinx.dom.clear import kotlinx.dom.clear
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.dom.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
@ -104,32 +103,13 @@ public fun ThreeView(
} }
) { ) {
if (solid == null) { if (solid == null) {
Div({ Div({ classes("d-flex", "justify-content-center") }) {
style { Div({
position(Position.Fixed) classes("spinner-border")
width(100.percent) attr("role", "status")
height(100.percent) }) {
zIndex(1000) Span({ classes("visually-hidden") }) {
top(40.percent) Text("Loading...")
left(0.px)
opacity(0.5)
@OptIn(ExperimentalComposeWebApi::class) filter {
opacity(50.percent)
}
}
}) {
Div({ classes("d-flex", " justify-content-center") }) {
Div({
classes("spinner-grow", "text-primary")
style {
width(3.cssRem)
height(3.cssRem)
zIndex(20)
}
attr("role", "status")
}) {
Span({ classes("sr-only") }) { Text("Loading 3D vision") }
} }
} }
} }
@ -144,33 +124,17 @@ public fun ThreeView(
else -> (solid as? SolidGroup)?.get(it) else -> (solid as? SolidGroup)?.get(it)
} }
}?.let { vision -> }?.let { vision ->
Card( Card(attrs = {
attrs = { style {
style { position(Position.Absolute)
position(Position.Absolute) top(10.px)
top(5.px) right(10.px)
right(5.px) width(450.px)
width(450.px) overflowY("auto")
overflowY("auto")
}
},
headerAttrs = {
style {
alignItems(AlignItems.Center)
}
},
header = {
NameCrumbs(selected) { selected = it }
},
footer = {
vision.styles.takeIf { it.isNotEmpty() }?.let { styles ->
P {
B { Text("Styles: ") }
Text(styles.joinToString(separator = ", "))
}
}
} }
) { }) {
NameCrumbs(selected) { selected = it }
Hr()
PropertyEditor( PropertyEditor(
scope = solids.context, scope = solids.context,
rootMeta = vision.properties.root(), rootMeta = vision.properties.root(),
@ -187,6 +151,13 @@ public fun ThreeView(
updates = vision.properties.changes, updates = vision.properties.changes,
rootDescriptor = vision.descriptor rootDescriptor = vision.descriptor
) )
vision.styles.takeIf { it.isNotEmpty() }?.let { styles ->
Hr()
P {
B { Text("Styles: ") }
Text(styles.joinToString(separator = ", "))
}
}
} }
} }
} }
@ -207,7 +178,13 @@ public fun ThreeView(
} }
} }
) { ) {
ThreeControls(solid, optionsSnapshot, selected, onSelect = { selected = it }, tabBuilder = sidebarTabs) ThreeControls(
solid,
optionsSnapshot,
selected,
onSelect = { selected = it },
tabBuilder = sidebarTabs
)
} }
} }
} else { } else {

View File

@ -6,21 +6,33 @@ plugins {
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
kscience { kscience {
fullStack("js/visionforge-three.js") fullStack(
bundleName = "js/visionforge-three.js",
browserConfig = {
webpackTask {
cssSupport {
enabled = true
}
scssSupport {
enabled = true
}
}
}
)
commonMain { commonMain {
api(projects.visionforgeSolid) api(projects.visionforgeSolid)
api(projects.visionforgeComposeHtml) api(projects.visionforgeComposeHtml)
} }
jvmMain{ jvmMain {
api(projects.visionforgeServer) api(projects.visionforgeServer)
} }
jsMain{ jsMain {
api(projects.visionforgeThreejs) api(projects.visionforgeThreejs)
implementation(npm("file-saver","2.0.5")) implementation(npm("file-saver", "2.0.5"))
implementation(npm("@types/file-saver", "2.0.7")) implementation(npm("@types/file-saver", "2.0.7"))
compileOnly(npm("webpack-bundle-analyzer","4.5.0")) compileOnly(npm("webpack-bundle-analyzer", "4.5.0"))
} }
} }