New layout

This commit is contained in:
Alexander Nozik 2019-10-09 12:38:39 +03:00
parent 9f157a80b9
commit 0fbad16be6
13 changed files with 161 additions and 176 deletions

View File

@ -27,7 +27,7 @@ abstract class AbstractVisualGroup : AbstractVisualObject(), MutableVisualGroup
override fun getStyle(name: Name): Meta? = styleSheet[name] override fun getStyle(name: Name): Meta? = styleSheet[name]
override fun addStyle(name: Name, meta: Meta) { override fun addStyle(name: Name, meta: Meta, apply: Boolean) {
fun VisualObject.applyStyle(name: Name, meta: Meta) { fun VisualObject.applyStyle(name: Name, meta: Meta) {
if (styles.contains(name)) { if (styles.contains(name)) {
//full update //full update
@ -45,7 +45,9 @@ abstract class AbstractVisualGroup : AbstractVisualObject(), MutableVisualGroup
} }
} }
styleSheet[name] = meta styleSheet[name] = meta
applyStyle(name, meta) if (apply) {
applyStyle(name, meta)
}
} }

View File

@ -41,7 +41,7 @@ interface VisualGroup : Provider, Iterable<VisualObject>, VisualObject {
/** /**
* Add or replace style with given name * Add or replace style with given name
*/ */
fun addStyle(name: Name, meta: Meta) fun addStyle(name: Name, meta: Meta, apply: Boolean = true)
operator fun get(name: Name): VisualObject? { operator fun get(name: Name): VisualObject? {
return when { return when {

View File

@ -1,4 +1,4 @@
package hep.dataforge.vis.hmr package hep.dataforge.vis
import kotlin.browser.document import kotlin.browser.document
import kotlin.dom.hasClass import kotlin.dom.hasClass

View File

@ -1,6 +1,4 @@
package hep.dataforge.vis.hmr package hep.dataforge.vis
import kotlinext.js.objectAssign
inline fun <T : Any> jsObject(builder: T.() -> Unit): T { inline fun <T : Any> jsObject(builder: T.() -> Unit): T {
val obj: T = js("({})") as T val obj: T = js("({})") as T

View File

@ -69,7 +69,7 @@ class GDMLTransformer(val root: GDML) {
internal fun finalize(final: VisualGroup3D): VisualGroup3D { internal fun finalize(final: VisualGroup3D): VisualGroup3D {
final.templates = templates final.templates = templates
styleCache.forEach { styleCache.forEach {
final.addStyle(it.key, it.value) final.addStyle(it.key, it.value, false)
} }
final.rotationOrder = RotationOrder.ZXY final.rotationOrder = RotationOrder.ZXY
onFinish(this@GDMLTransformer) onFinish(this@GDMLTransformer)

View File

@ -1,36 +1,26 @@
package hep.dataforge.vis.spatial.gdml.demo package hep.dataforge.vis.spatial.gdml.demo
import hep.dataforge.context.Global import hep.dataforge.context.Global
import hep.dataforge.vis.common.VisualGroup import hep.dataforge.vis.ApplicationBase
import hep.dataforge.vis.hmr.ApplicationBase
import hep.dataforge.vis.hmr.startApplication
import hep.dataforge.vis.spatial.Material3D.Companion.OPACITY_KEY import hep.dataforge.vis.spatial.Material3D.Companion.OPACITY_KEY
import hep.dataforge.vis.spatial.Visual3DPlugin import hep.dataforge.vis.spatial.Visual3DPlugin
import hep.dataforge.vis.spatial.VisualGroup3D import hep.dataforge.vis.spatial.VisualGroup3D
import hep.dataforge.vis.spatial.VisualObject3D import hep.dataforge.vis.spatial.VisualObject3D
import hep.dataforge.vis.spatial.attachChildren import hep.dataforge.vis.spatial.attachChildren
import hep.dataforge.vis.spatial.editor.propertyEditor
import hep.dataforge.vis.spatial.editor.threeOutputConfig
import hep.dataforge.vis.spatial.editor.visualObjectTree
import hep.dataforge.vis.spatial.gdml.GDMLTransformer import hep.dataforge.vis.spatial.gdml.GDMLTransformer
import hep.dataforge.vis.spatial.gdml.LUnit import hep.dataforge.vis.spatial.gdml.LUnit
import hep.dataforge.vis.spatial.gdml.toVisual import hep.dataforge.vis.spatial.gdml.toVisual
import hep.dataforge.vis.spatial.three.ThreeOutput
import hep.dataforge.vis.spatial.three.ThreePlugin import hep.dataforge.vis.spatial.three.ThreePlugin
import hep.dataforge.vis.spatial.three.output import hep.dataforge.vis.spatial.three.output
import hep.dataforge.vis.spatial.transform.RemoveSingleChild import hep.dataforge.vis.startApplication
import hep.dataforge.vis.spatial.transform.UnRef
import hep.dataforge.vis.spatial.transform.transformInPlace
import hep.dataforge.vis.spatial.editor.propertyEditor
import hep.dataforge.vis.spatial.editor.render
import hep.dataforge.vis.spatial.editor.toTree
import kotlinx.html.InputType
import kotlinx.html.dom.append import kotlinx.html.dom.append
import kotlinx.html.js.input
import kotlinx.html.js.li
import kotlinx.html.js.p import kotlinx.html.js.p
import kotlinx.html.js.ul import org.w3c.dom.DragEvent
import org.w3c.dom.Element
import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event
import org.w3c.files.FileList import org.w3c.files.FileList
import org.w3c.files.FileReader import org.w3c.files.FileReader
import org.w3c.files.get import org.w3c.files.get
@ -43,33 +33,27 @@ private class GDMLDemoApp : ApplicationBase() {
/** /**
* Handle mouse drag according to https://www.html5rocks.com/en/tutorials/file/dndfiles/ * Handle mouse drag according to https://www.html5rocks.com/en/tutorials/file/dndfiles/
*/ */
private fun handleDragOver(event: Event) { private fun handleDragOver(event: DragEvent) {
event.stopPropagation() event.stopPropagation()
event.preventDefault() event.preventDefault()
event.asDynamic().dataTransfer.dropEffect = "copy" event.dataTransfer?.dropEffect = "copy"
} }
/** /**
* Load data from text file * Load data from text file
*/ */
private fun loadData(event: Event, block: (name: String, data: String) -> Unit) { private fun loadData(event: DragEvent, block: (name: String, data: String) -> Unit) {
event.stopPropagation() event.stopPropagation()
event.preventDefault() event.preventDefault()
val file = (event.asDynamic().dataTransfer.files as FileList)[0] val file = (event.dataTransfer?.files as FileList)[0]
?: throw RuntimeException("Failed to load file") ?: throw RuntimeException("Failed to load file")
FileReader().apply { FileReader().apply {
onload = { onload = {
val string = result as String val string = result as String
// try {
block(file.name, string) block(file.name, string)
// } catch (ex: Exception) {
// console.error(ex)
// }
} }
readAsText(file) readAsText(file)
} }
@ -102,52 +86,22 @@ private class GDMLDemoApp : ApplicationBase() {
} }
} }
fun setupLayers(element: Element, output: ThreeOutput) {
element.clear()
element.append {
ul("list-group") {
(0..9).forEach { layer ->
li("list-group-item") {
+"layer $layer"
input(type = InputType.checkBox).apply {
if (layer == 0) {
checked = true
}
onchange = {
if (checked) {
output.camera.layers.enable(layer)
} else {
output.camera.layers.disable(layer)
}
}
}
}
}
}
}
}
private val gdmlConfiguration: GDMLTransformer.() -> Unit = { private val gdmlConfiguration: GDMLTransformer.() -> Unit = {
lUnit = LUnit.CM lUnit = LUnit.CM
volumeAction = { volume -> volumeAction = { volume ->
when { when {
volume.name.startsWith("ecal01lay") -> GDMLTransformer.Action.REJECT volume.name.startsWith("ecal01lay") -> GDMLTransformer.Action.REJECT
// volume.name.startsWith("ecal") -> GDMLTransformer.Action.CACHE
volume.name.startsWith("UPBL") -> GDMLTransformer.Action.REJECT volume.name.startsWith("UPBL") -> GDMLTransformer.Action.REJECT
volume.name.startsWith("USCL") -> GDMLTransformer.Action.REJECT volume.name.startsWith("USCL") -> GDMLTransformer.Action.REJECT
// volume.name.startsWith("U") -> GDMLTransformer.Action.CACHE
volume.name.startsWith("VPBL") -> GDMLTransformer.Action.REJECT volume.name.startsWith("VPBL") -> GDMLTransformer.Action.REJECT
volume.name.startsWith("VSCL") -> GDMLTransformer.Action.REJECT volume.name.startsWith("VSCL") -> GDMLTransformer.Action.REJECT
// volume.name.startsWith("V") -> GDMLTransformer.Action.CACHE
else -> GDMLTransformer.Action.CACHE else -> GDMLTransformer.Action.CACHE
} }
} }
solidConfiguration = { parent, solid -> solidConfiguration = { parent, solid ->
if ( if (
solid.name.startsWith("Coil") solid.name.startsWith("Yoke")
|| solid.name.startsWith("Yoke")
|| solid.name.startsWith("Magnet")
|| solid.name.startsWith("Pole") || solid.name.startsWith("Pole")
|| parent.physVolumes.isNotEmpty() || parent.physVolumes.isNotEmpty()
) { ) {
@ -193,17 +147,13 @@ private class GDMLDemoApp : ApplicationBase() {
//(visual as? VisualGroup3D)?.transformInPlace(UnRef, RemoveSingleChild) //(visual as? VisualGroup3D)?.transformInPlace(UnRef, RemoveSingleChild)
message("Rendering") message("Rendering")
val output = three.output(canvas as HTMLElement)
//output.camera.layers.enable(1) //output.camera.layers.enable(1)
output.camera.layers.set(0) val output = three.output(canvas as HTMLElement)
setupLayers(layers, output)
if (visual is VisualGroup) { output.camera.layers.set(0)
visual.toTree(editor::propertyEditor).render(tree as HTMLElement) { layers.threeOutputConfig(output)
showCheckboxes = false tree.visualObjectTree(visual, editor::propertyEditor)
}
}
output.render(visual) output.render(visual)
message(null) message(null)
@ -211,8 +161,8 @@ private class GDMLDemoApp : ApplicationBase() {
} }
(document.getElementById("drop_zone") as? HTMLDivElement)?.apply { (document.getElementById("drop_zone") as? HTMLDivElement)?.apply {
addEventListener("dragover", { handleDragOver(it) }, false) addEventListener("dragover", { handleDragOver(it as DragEvent) }, false)
addEventListener("drop", { loadData(it, action) }, false) addEventListener("drop", { loadData(it as DragEvent, action) }, false)
} }
} }

View File

@ -29,45 +29,12 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-9" id="canvas"></div> <div class="col-lg-3" id="tree"></div>
<div class="col-3"> <div class="col-lg-6">
<div class="row" id="layers"></div>
<div id="editor"></div> <div class="row container" id="canvas"></div>
<div class="accordion" id="accordion">
<div class="card">
<div class="card-header" id="layers-header">
<h2 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#layers-body"
aria-expanded="true" aria-controls="layers-body">
Layers
</button>
</h2>
</div>
<div id="layers-body" class="collapse show" aria-labelledby="layers-header"
data-parent="#accordion">
<div class="card-body">
<div id="layers"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="tree-header">
<h2 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse"
data-target="#tree-body" aria-expanded="false" aria-controls="tree-body">
Object tree
</button>
</h2>
</div>
<div id="tree-body" class="collapse" aria-labelledby="tree-header" data-parent="#accordion">
<div class="card-body">
<div id="tree"></div>
</div>
</div>
</div>
</div>
</div> </div>
<div class="col-lg-3" id="editor"></div>
</div> </div>
</div> </div>

View File

@ -5,8 +5,14 @@ package hep.dataforge.vis.spatial
import hep.dataforge.io.ConfigSerializer import hep.dataforge.io.ConfigSerializer
import hep.dataforge.io.NameSerializer import hep.dataforge.io.NameSerializer
import hep.dataforge.meta.* import hep.dataforge.meta.*
import hep.dataforge.names.* import hep.dataforge.names.Name
import hep.dataforge.vis.common.* import hep.dataforge.names.NameToken
import hep.dataforge.names.asName
import hep.dataforge.names.plus
import hep.dataforge.vis.common.AbstractVisualObject
import hep.dataforge.vis.common.MutableVisualGroup
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.VisualObject
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers import kotlinx.serialization.UseSerializers
import kotlin.collections.component1 import kotlin.collections.component1
@ -35,16 +41,21 @@ class Proxy(val templateName: Name) : AbstractVisualObject(), VisualGroup, Visua
override fun getStyle(name: Name): Meta? = (parent as VisualGroup?)?.getStyle(name) override fun getStyle(name: Name): Meta? = (parent as VisualGroup?)?.getStyle(name)
override fun addStyle(name: Name, meta: Meta) { override fun addStyle(name: Name, meta: Meta, apply: Boolean) {
(parent as VisualGroup?)?.addStyle(name, meta) (parent as VisualGroup?)?.addStyle(name, meta, apply)
//do nothing //do nothing
} }
override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? {
return if (inherit) { return if (inherit) {
super.getProperty(name, false) ?: prototype.getProperty(name, false) ?: parent?.getProperty(name, inherit) properties?.get(name)
?: mergedStyles[name]
?: prototype.getProperty(name, false)
?: parent?.getProperty(name, inherit)
} else { } else {
super.getProperty(name, false) ?: prototype.getProperty(name, false) properties?.get(name)
?: mergedStyles[name]
?: prototype.getProperty(name, false)
} }
} }
@ -93,8 +104,8 @@ class Proxy(val templateName: Name) : AbstractVisualObject(), VisualGroup, Visua
override fun getStyle(name: Name): Meta? = this@Proxy.getStyle(name) override fun getStyle(name: Name): Meta? = this@Proxy.getStyle(name)
override fun addStyle(name: Name, meta: Meta) { override fun addStyle(name: Name, meta: Meta, apply: Boolean) {
this@Proxy.addStyle(name, meta) this@Proxy.addStyle(name, meta, apply)
} }
override var properties: Config? override var properties: Config?
@ -118,8 +129,8 @@ class Proxy(val templateName: Name) : AbstractVisualObject(), VisualGroup, Visua
return if (inherit) { return if (inherit) {
properties?.get(name) properties?.get(name)
?: mergedStyles[name] ?: mergedStyles[name]
?: parent?.getProperty(name, inherit)
?: prototype.getProperty(name, inherit) ?: prototype.getProperty(name, inherit)
?: parent?.getProperty(name, inherit)
} else { } else {
properties?.get(name) properties?.get(name)
?: mergedStyles[name] ?: mergedStyles[name]

View File

@ -0,0 +1,15 @@
package hep.dataforge.vis.spatial.editor
import kotlinx.html.TagConsumer
import kotlinx.html.js.div
import kotlinx.html.js.h3
import org.w3c.dom.HTMLElement
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()
}
}
}

View File

@ -9,21 +9,19 @@ import hep.dataforge.names.NameToken
import hep.dataforge.vis.common.VisualGroup import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.VisualObject import hep.dataforge.vis.common.VisualObject
import hep.dataforge.vis.common.getProperty import hep.dataforge.vis.common.getProperty
import hep.dataforge.vis.jsObject
import hep.dataforge.vis.spatial.Proxy import hep.dataforge.vis.spatial.Proxy
import hep.dataforge.vis.spatial.visible import hep.dataforge.vis.spatial.visible
import org.w3c.dom.HTMLElement import info.laht.threekt.loaders.Cache.clear
import kotlinx.html.div
import kotlinx.html.dom.append
import org.w3c.dom.Element
import kotlin.js.json import kotlin.js.json
operator fun Name.plus(other: NameToken): Name = Name(tokens + other) operator fun Name.plus(other: NameToken): Name = Name(tokens + other)
fun InspireTree.render(element: HTMLElement, block: DomConfig.() -> Unit = {}) {
val config = (json(
"target" to element
) as DomConfig).apply(block)
InspireTreeDOM(this, config)
}
internal fun createInspireTree(block: Config.() -> Unit = {}): InspireTree { private fun createInspireTree(block: Config.() -> Unit = {}): InspireTree {
val config = (json( val config = (json(
"checkbox" to json( "checkbox" to json(
"autoCheckChildren" to false "autoCheckChildren" to false
@ -32,7 +30,7 @@ internal fun createInspireTree(block: Config.() -> Unit = {}): InspireTree {
return InspireTree(config) return InspireTree(config)
} }
fun VisualGroup.toTree(onFocus: (VisualObject?, String?) -> Unit = { _, _ -> }): InspireTree { private fun VisualObject.toTree(onFocus: (VisualObject?, String?) -> Unit = { _, _ -> }): InspireTree {
val map = HashMap<String, VisualObject>() val map = HashMap<String, VisualObject>()
@ -67,15 +65,17 @@ fun VisualGroup.toTree(onFocus: (VisualObject?, String?) -> Unit = { _, _ -> }):
} }
fun TreeNode.fillChildren(group: VisualGroup, groupName: Name) { fun TreeNode.fillChildren(group: VisualObject, groupName: Name) {
group.children.forEach { (token, obj) -> if(group is VisualGroup) {
if(! token.body.startsWith("@")) { group.children.forEach { (token, obj) ->
val name = groupName + token if (!token.body.startsWith("@")) {
val nodeConfig = generateNodeConfig(obj, name) val name = groupName + token
val childNode = addChild(nodeConfig) val nodeConfig = generateNodeConfig(obj, name)
map[childNode.id] = obj val childNode = addChild(nodeConfig)
if (obj is VisualGroup) { map[childNode.id] = obj
childNode.fillChildren(obj, name) if (obj is VisualGroup) {
childNode.fillChildren(obj, name)
}
} }
} }
} }
@ -121,3 +121,16 @@ fun VisualGroup.toTree(onFocus: (VisualObject?, String?) -> Unit = { _, _ -> }):
return inspireTree return inspireTree
} }
fun Element.visualObjectTree(group: VisualObject, onFocus: (VisualObject?, String?) -> Unit) {
clear()
append {
card("Visual object tree") {
val domConfig = jsObject<DomConfig> {
target = div()
showCheckboxes = false
}
InspireTreeDOM(group.toTree(onFocus), domConfig)
}
}
}

View File

@ -0,0 +1,37 @@
package hep.dataforge.vis.spatial.editor
import hep.dataforge.vis.spatial.three.ThreeOutput
import kotlinx.html.InputType
import kotlinx.html.dom.append
import kotlinx.html.js.div
import kotlinx.html.js.input
import kotlinx.html.js.label
import org.w3c.dom.Element
import kotlin.dom.clear
fun Element.threeOutputConfig(output: ThreeOutput) {
clear()
append {
card("Layers"){
div("row") {
(0..11).forEach { layer ->
div("col-1") {
label { +layer.toString() }
input(type = InputType.checkBox).apply {
if (layer == 0) {
checked = true
}
onchange = {
if (checked) {
output.camera.layers.enable(layer)
} else {
output.camera.layers.disable(layer)
}
}
}
}
}
}
}
}
}

View File

@ -2,10 +2,9 @@ package hep.dataforge.vis.spatial.editor
import hep.dataforge.io.toJson import hep.dataforge.io.toJson
import hep.dataforge.meta.* import hep.dataforge.meta.*
import hep.dataforge.names.toName
import hep.dataforge.vis.common.VisualObject import hep.dataforge.vis.common.VisualObject
import hep.dataforge.vis.common.findStyle import hep.dataforge.vis.common.findStyle
import hep.dataforge.vis.hmr.jsObject import hep.dataforge.vis.jsObject
import hep.dataforge.vis.spatial.Material3D.Companion.COLOR_KEY import hep.dataforge.vis.spatial.Material3D.Companion.COLOR_KEY
import hep.dataforge.vis.spatial.Material3D.Companion.OPACITY_KEY import hep.dataforge.vis.spatial.Material3D.Companion.OPACITY_KEY
import hep.dataforge.vis.spatial.VisualObject3D.Companion.VISIBLE_KEY import hep.dataforge.vis.spatial.VisualObject3D.Companion.VISIBLE_KEY
@ -15,11 +14,11 @@ import hep.dataforge.vis.spatial.prototype
import hep.dataforge.vis.spatial.visible import hep.dataforge.vis.spatial.visible
import kotlinx.html.dom.append import kotlinx.html.dom.append
import kotlinx.html.js.div import kotlinx.html.js.div
import kotlinx.html.js.h3
import kotlinx.html.js.h4 import kotlinx.html.js.h4
import org.w3c.dom.Element import org.w3c.dom.Element
import kotlin.dom.clear import kotlin.dom.clear
//FIXME something rotten in JS-Meta converter
fun Meta.toDynamic() = JSON.parse<dynamic>(toJson().toString()) fun Meta.toDynamic() = JSON.parse<dynamic>(toJson().toString())
@ -27,41 +26,34 @@ fun Element.propertyEditor(item: VisualObject?, name: String?) {
clear() clear()
if (item != null) { if (item != null) {
append { append {
div("card") { card("Properties") {
div("card-body") { val config = (item.properties ?: item.prototype?.properties) ?: EmptyMeta
h3(classes = "card-title") { +"Properties" } val metaToEdit = config.builder().apply {
}.apply { VISIBLE_KEY to (item.visible ?: true)
val config = (item.properties ?: item.prototype?.properties) ?: EmptyMeta COLOR_KEY to (item.color ?: "#ffffff")
val metaToEdit = config.builder().apply { OPACITY_KEY to (item.opacity ?: 1.0)
VISIBLE_KEY to (item.visible ?: true)
COLOR_KEY to (item.color ?: "#ffffff")
OPACITY_KEY to (item.opacity ?: 1.0)
}
//FIXME something rotten in JS-Meta converter
val dMeta: dynamic = metaToEdit.toDynamic()
//jsObject.material.color != null
val options: JSONEditorOptions = jsObject{
mode = "form"
onChangeJSON = { item.config.update(DynamicMeta(it.asDynamic())) }
}
JSONEditor(this, options, dMeta)
} }
val dMeta: dynamic = metaToEdit.toDynamic()
val options: JSONEditorOptions = jsObject {
mode = "form"
onChangeJSON = { item.config.update(DynamicMeta(it.asDynamic())) }
}
JSONEditor(div(), options, dMeta)
} }
val styles = item.styles
div("card") { if (styles.isNotEmpty()) {
div("card-body") { card("Styles") {
h3(classes = "card-title") { +"Styles" } item.styles.forEach { style ->
} val styleMeta = item.findStyle(style)
item.styles.forEach { style -> h4("container") { +style.toString() }
val styleMeta = item.findStyle(style) if (styleMeta != null) {
h4 { +style.toString() } div("container").apply {
if (styleMeta != null) { val options: JSONEditorOptions = jsObject {
div("container").apply { mode = "view"
val options: JSONEditorOptions = jsObject{ }
mode = "view" JSONEditor(this, options, styleMeta.toDynamic())
} }
JSONEditor(this, options, styleMeta.toDynamic())
} }
} }
} }

View File

@ -1,10 +1,10 @@
package hep.dataforge.vis.spatial.demo package hep.dataforge.vis.spatial.demo
import hep.dataforge.context.ContextBuilder import hep.dataforge.context.ContextBuilder
import hep.dataforge.vis.ApplicationBase
import hep.dataforge.vis.common.Colors import hep.dataforge.vis.common.Colors
import hep.dataforge.vis.hmr.ApplicationBase
import hep.dataforge.vis.hmr.startApplication
import hep.dataforge.vis.spatial.* import hep.dataforge.vis.spatial.*
import hep.dataforge.vis.startApplication
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive