Compare commits

...

3 Commits

14 changed files with 155 additions and 91 deletions

View File

@ -1,12 +1,31 @@
# Changelog
## [Unreleased]
## Unreleased
### Added
### Changed
- **Breaking API** Move vision cache to upper level for renderers to avoid re-creating visions for page reload.
- **Breaking API** Forms refactor
### Deprecated
### Removed
### Fixed
### Security
## 0.3.0 - 2023-12-23
### Added
- Context receivers flag
- MeshLine for thick lines
- Custom client-side events and thier processing in VisionServer
### Changed
- Color accessor property is now `colorProperty`. Color uses non-nullable `invoke` instead of `set`.
- API update for server and pages
- Edges moved to solids module for easier construction
@ -17,17 +36,14 @@
- Naming of Canvas3D options.
- Lights are added to the scene instead of 3D options.
### Deprecated
### Removed
### Fixed
- Jupyter integration for IDEA and Jupyter lab.
### Security
## 0.2.0
## [0.2.0]
### Added
- Server module
- Change collector
- Customizable accessors for colors
@ -38,8 +54,8 @@
- Markdown module
- Tables module
### Changed
- Vision does not implement ItemProvider anymore. Property changes are done via `getProperty`/`setProperty` and `property` delegate.
- Point3D and Point2D are made separate classes instead of expect/actual (to split up different engines.
- JavaFX support moved to a separate module
@ -54,16 +70,10 @@
- Property listeners are not triggered if there are no changes.
- Feedback websocket connection in the client.
### Deprecated
### Removed
- Primary modules dependencies on UI
### Fixed
- Version conflicts
### Security

View File

@ -11,7 +11,7 @@ val dataforgeVersion by extra("0.7.1")
allprojects {
group = "space.kscience"
version = "0.3.0"
version = "0.3.1"
}
subprojects {

View File

@ -4,6 +4,7 @@ import ringui.SmartTabs
import ringui.Tab
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.request
import space.kscience.plotly.Plotly.plot
import space.kscience.plotly.models.Trace
import space.kscience.plotly.scatter
import space.kscience.visionforge.Application
@ -97,7 +98,7 @@ private class JsPlaygroundApp : Application {
Tab("plotly") {
Plotly {
attrs {
plot = space.kscience.plotly.Plotly.plot {
plot = plot {
scatter {
x(1, 2, 3)
y(5, 8, 7)

View File

@ -11,6 +11,7 @@ import space.kscience.visionforge.markup.VisionOfMarkup
import space.kscience.visionforge.react.flexRow
import space.kscience.visionforge.ring.ThreeCanvasWithControls
import space.kscience.visionforge.ring.solid
import space.kscience.visionforge.setAsRoot
import space.kscience.visionforge.solid.*
import styled.css
import styled.styledDiv
@ -27,7 +28,9 @@ val GravityDemo = fc<DemoProps> { props ->
val energyTrace = Trace {
name = "energy"
}
val markup = VisionOfMarkup()
val markup = VisionOfMarkup().apply {
setAsRoot(props.solids.visionManager)
}
styledDiv {
css {

View File

@ -10,7 +10,7 @@ import space.kscience.dataforge.context.request
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.bindForm
import space.kscience.visionforge.html.visionOfForm
import space.kscience.visionforge.onPropertyChange
import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser
@ -36,7 +36,7 @@ fun main() {
visionManager,
VisionPage.scriptHeader("js/visionforge-playground.js"),
) {
bindForm(form) {
visionOfForm(form) {
label {
htmlFor = "fname"
+"First name:"
@ -67,8 +67,8 @@ fun main() {
value = "Submit"
}
}
println(form.values)
vision(form)
println(form.values)
}
}.start(false)

View File

@ -9,7 +9,9 @@ kscience {
// useSerialization {
// json()
// }
jvm()
jvm{
withJava()
}
jvmMain{
implementation("io.ktor:ktor-server-cio")
implementation(projects.visionforgeThreejs.visionforgeThreejsServer)

View File

@ -750,10 +750,10 @@ public abstract interface class space/kscience/visionforge/html/HtmlVisionFragme
public final class space/kscience/visionforge/html/HtmlVisionRendererKt {
public static final fun appendTo (Lspace/kscience/visionforge/html/HtmlVisionFragment;Lspace/kscience/visionforge/html/VisionTagConsumer;)V
public static final fun visionFragment (Lkotlinx/html/FlowContent;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Lspace/kscience/visionforge/html/HtmlVisionFragment;)V
public static final fun visionFragment (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lspace/kscience/visionforge/html/HtmlVisionFragment;)V
public static synthetic fun visionFragment$default (Lkotlinx/html/FlowContent;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Lspace/kscience/visionforge/html/HtmlVisionFragment;ILjava/lang/Object;)V
public static synthetic fun visionFragment$default (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lspace/kscience/visionforge/html/HtmlVisionFragment;ILjava/lang/Object;)V
public static final fun visionFragment (Lkotlinx/html/FlowContent;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lspace/kscience/visionforge/html/HtmlVisionFragment;)V
public static final fun visionFragment (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lspace/kscience/visionforge/html/HtmlVisionFragment;)V
public static synthetic fun visionFragment$default (Lkotlinx/html/FlowContent;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lspace/kscience/visionforge/html/HtmlVisionFragment;ILjava/lang/Object;)V
public static synthetic fun visionFragment$default (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/VisionManager;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lspace/kscience/visionforge/html/HtmlVisionFragment;ILjava/lang/Object;)V
}
public final class space/kscience/visionforge/html/InputFeedbackMode : java/lang/Enum {
@ -782,6 +782,21 @@ public final class space/kscience/visionforge/html/ResourceLocation : java/lang/
public abstract interface annotation class space/kscience/visionforge/html/VisionDSL : java/lang/annotation/Annotation {
}
public final class space/kscience/visionforge/html/VisionDisplay {
public fun <init> (Lspace/kscience/visionforge/VisionManager;Lspace/kscience/visionforge/Vision;Lspace/kscience/dataforge/meta/Meta;)V
public final fun component1 ()Lspace/kscience/visionforge/VisionManager;
public final fun component2 ()Lspace/kscience/visionforge/Vision;
public final fun component3 ()Lspace/kscience/dataforge/meta/Meta;
public final fun copy (Lspace/kscience/visionforge/VisionManager;Lspace/kscience/visionforge/Vision;Lspace/kscience/dataforge/meta/Meta;)Lspace/kscience/visionforge/html/VisionDisplay;
public static synthetic fun copy$default (Lspace/kscience/visionforge/html/VisionDisplay;Lspace/kscience/visionforge/VisionManager;Lspace/kscience/visionforge/Vision;Lspace/kscience/dataforge/meta/Meta;ILjava/lang/Object;)Lspace/kscience/visionforge/html/VisionDisplay;
public fun equals (Ljava/lang/Object;)Z
public final fun getMeta ()Lspace/kscience/dataforge/meta/Meta;
public final fun getVision ()Lspace/kscience/visionforge/Vision;
public final fun getVisionManager ()Lspace/kscience/visionforge/VisionManager;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class space/kscience/visionforge/html/VisionOfCheckbox : space/kscience/visionforge/html/VisionOfHtmlInput {
public static final field Companion Lspace/kscience/visionforge/html/VisionOfCheckbox$Companion;
public fun <init> ()V
@ -852,7 +867,7 @@ public final class space/kscience/visionforge/html/VisionOfHtmlControl$Companion
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class space/kscience/visionforge/html/VisionOfHtmlForm : space/kscience/visionforge/html/VisionOfHtmlControl {
public final class space/kscience/visionforge/html/VisionOfHtmlForm : space/kscience/visionforge/html/VisionOfHtmlControl, space/kscience/visionforge/ClickControl {
public static final field Companion Lspace/kscience/visionforge/html/VisionOfHtmlForm$Companion;
public fun <init> (Ljava/lang/String;)V
public final fun getFormId ()Ljava/lang/String;
@ -876,9 +891,11 @@ public final class space/kscience/visionforge/html/VisionOfHtmlForm$Companion {
}
public final class space/kscience/visionforge/html/VisionOfHtmlFormKt {
public static final fun bindForm (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/html/VisionOfHtmlForm;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static final fun button (Lspace/kscience/visionforge/html/VisionOutput;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lspace/kscience/visionforge/html/VisionOfHtmlButton;
public static synthetic fun button$default (Lspace/kscience/visionforge/html/VisionOutput;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lspace/kscience/visionforge/html/VisionOfHtmlButton;
public static final fun onSubmit (Lspace/kscience/visionforge/html/VisionOfHtmlForm;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/Job;
public static final fun visionOfForm (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/html/VisionOfHtmlForm;Ljava/lang/String;Lkotlinx/html/FormEncType;Lkotlinx/html/FormMethod;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static synthetic fun visionOfForm$default (Lkotlinx/html/TagConsumer;Lspace/kscience/visionforge/html/VisionOfHtmlForm;Ljava/lang/String;Lkotlinx/html/FormEncType;Lkotlinx/html/FormMethod;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
}
public class space/kscience/visionforge/html/VisionOfHtmlInput : space/kscience/visionforge/html/VisionOfHtmlControl {

View File

@ -39,7 +39,8 @@ public interface ControlVision : Vision {
@Serializable
@SerialName("control.click")
public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() {
public val payload: Meta? by meta.node()
public val payload: Meta get() = meta[::payload.name] ?: Meta.EMPTY
public val name: Name? get() = meta["name"].string?.parseAsName()
override fun toString(): String = meta.toString()

View File

@ -14,13 +14,17 @@ public fun interface HtmlVisionFragment{
public fun HtmlVisionFragment.appendTo(consumer: VisionTagConsumer<*>): Unit = consumer.append()
public data class VisionDisplay(val visionManager: VisionManager, val vision: Vision, val meta: Meta)
/**
* Render a fragment in the given consumer and return a map of extracted visions
* @param context a context used to create a vision fragment
* @param visionManager a context plugin used to create a vision fragment
* @param embedData embed Vision initial state in the HTML
* @param fetchDataUrl fetch data after first render from given url
* @param updatesUrl receive push updates from the server at given url
* @param idPrefix a prefix to be used before vision ids
* @param displayCache external cache for Vision displays. It is required to avoid re-creating visions on page update
* @param fragment the fragment to render
*/
public fun TagConsumer<*>.visionFragment(
visionManager: VisionManager,
@ -28,39 +32,31 @@ public fun TagConsumer<*>.visionFragment(
fetchDataUrl: String? = null,
updatesUrl: String? = null,
idPrefix: String? = null,
onVisionRendered: (Name, Vision) -> Unit = { _, _ -> },
displayCache: MutableMap<Name, VisionDisplay> = mutableMapOf(),
fragment: HtmlVisionFragment,
) {
val collector: MutableMap<Name, Pair<VisionOutput, Vision>> = mutableMapOf()
val consumer = object : VisionTagConsumer<Any?>(this@visionFragment, visionManager, idPrefix) {
override fun <T> TagConsumer<T>.vision(name: Name?, buildOutput: VisionOutput.() -> Vision): T {
//Avoid re-creating cached visions
val actualName = name ?: NameToken(
DEFAULT_VISION_NAME,
buildOutput.hashCode().toUInt().toString()
buildOutput.hashCode().toString(16)
).asName()
val (output, vision) = collector.getOrPut(actualName) {
val display = displayCache.getOrPut(actualName) {
val output = VisionOutput(context, actualName)
val vision = output.buildOutput()
onVisionRendered(actualName, vision)
output to vision
VisionDisplay(output.visionManager, vision, output.meta)
}
return addVision(actualName, output.visionManager, vision, output.meta)
return addVision(actualName, display.visionManager, display.vision, display.meta)
}
override fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta) {
val (_, actualVision) = collector.getOrPut(name) {
val output = VisionOutput(context, name)
onVisionRendered(name, vision)
output to vision
}
displayCache[name] = VisionDisplay(manager, vision, outputMeta)
// Toggle update mode
updatesUrl?.let {
@ -76,7 +72,7 @@ public fun TagConsumer<*>.visionFragment(
type = "text/json"
attributes["class"] = OUTPUT_DATA_CLASS
unsafe {
+"\n${manager.encodeToString(actualVision)}\n"
+"\n${manager.encodeToString(vision)}\n"
}
}
}
@ -91,8 +87,8 @@ public fun FlowContent.visionFragment(
embedData: Boolean = true,
fetchDataUrl: String? = null,
updatesUrl: String? = null,
onVisionRendered: (Name, Vision) -> Unit = { _, _ -> },
idPrefix: String? = null,
displayCache: MutableMap<Name, VisionDisplay> = mutableMapOf(),
fragment: HtmlVisionFragment,
): Unit = consumer.visionFragment(
visionManager = visionManager,
@ -100,6 +96,6 @@ public fun FlowContent.visionFragment(
fetchDataUrl = fetchDataUrl,
updatesUrl = updatesUrl,
idPrefix = idPrefix,
onVisionRendered = onVisionRendered,
displayCache = displayCache,
fragment = fragment
)

View File

@ -1,15 +1,15 @@
package space.kscience.visionforge.html
import kotlinx.html.FORM
import kotlinx.html.TagConsumer
import kotlinx.html.form
import kotlinx.html.id
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.html.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node
import space.kscience.dataforge.meta.string
import space.kscience.visionforge.ClickControl
import space.kscience.visionforge.onClick
/**
* @param formId an id of the element in rendered DOM, this form is bound to
@ -18,19 +18,31 @@ import space.kscience.visionforge.ClickControl
@SerialName("html.form")
public class VisionOfHtmlForm(
public val formId: String,
) : VisionOfHtmlControl() {
) : VisionOfHtmlControl(), ClickControl {
public var values: Meta? by properties.node()
}
public fun <R> TagConsumer<R>.bindForm(
visionOfForm: VisionOfHtmlForm,
builder: FORM.() -> Unit,
): R = form {
this.id = visionOfForm.formId
builder()
/**
* Create a [VisionOfHtmlForm] and bind this form to the id
*/
@HtmlTagMarker
public inline fun <T, C : TagConsumer<T>> C.visionOfForm(
vision: VisionOfHtmlForm,
action: String? = null,
encType: FormEncType? = null,
method: FormMethod? = null,
classes: String? = null,
crossinline block: FORM.() -> Unit = {},
) : T = form(action, encType, method, classes){
this.id = vision.formId
block()
}
public fun VisionOfHtmlForm.onSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onClick(scope) { block(payload) }
@Serializable
@SerialName("html.button")
public class VisionOfHtmlButton : VisionOfHtmlControl(), ClickControl {

View File

@ -15,8 +15,10 @@ import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.VisionOfHtmlButton
import space.kscience.visionforge.html.VisionOfHtmlForm
internal fun FormData.toMeta(): Meta {
/**
* Convert form data to Meta
*/
public fun FormData.toMeta(): Meta {
@Suppress("UNUSED_VARIABLE") val formData = this
//val res = js("Object.fromEntries(formData);")
val `object` = js("{}")
@ -67,8 +69,10 @@ internal val formVisionRenderer: ElementVisionRenderer =
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
client.sendMetaEvent(name, formData)
console.info("Sent: ${formData.toMap()}")
client.context.launch {
client.sendEvent(name, VisionClickEvent(name = name, payload = formData))
}
console.info("Sent form data: ${formData.toMap()}")
false
}
}

View File

@ -17,9 +17,9 @@ import space.kscience.dataforge.context.info
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionDisplay
import space.kscience.visionforge.html.visionFragment
import space.kscience.visionforge.server.VisionRoute
import space.kscience.visionforge.server.serveVisionData
@ -142,7 +142,7 @@ public class VisionForge(
//server.serveVisionsFromFragment(consumer, "content-${counter++}", fragment)
val cellRoute = "content-${counter++}"
val collector: MutableMap<Name, Vision> = mutableMapOf()
val cache: MutableMap<Name, VisionDisplay> = mutableMapOf()
val url = engine.environment.connectors.first().let {
url {
@ -153,13 +153,13 @@ public class VisionForge(
}
}
engine.application.serveVisionData(VisionRoute(cellRoute, visionManager), collector)
engine.application.serveVisionData(VisionRoute(cellRoute, visionManager), cache)
visionFragment(
visionManager,
embedData = true,
updatesUrl = url,
onVisionRendered = { name, vision -> collector[name] = vision },
displayCache = cache,
fragment = fragment
)
} else {

View File

@ -1,31 +1,50 @@
package space.kscience.visionforge.server
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.html.*
import io.ktor.server.http.content.*
import io.ktor.server.plugins.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.URLProtocol
import io.ktor.http.path
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.application.log
import io.ktor.server.engine.EngineConnectorConfig
import io.ktor.server.html.respondHtml
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.request.header
import io.ktor.server.request.host
import io.ktor.server.request.port
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.*
import io.ktor.server.util.*
import io.ktor.server.websocket.*
import io.ktor.util.pipeline.*
import io.ktor.websocket.*
import io.ktor.server.util.getOrFail
import io.ktor.server.util.url
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.application
import io.ktor.server.websocket.webSocket
import io.ktor.websocket.Frame
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.html.*
import kotlinx.html.body
import kotlinx.html.head
import kotlinx.html.header
import kotlinx.html.meta
import kotlinx.serialization.encodeToString
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.Configurable
import space.kscience.dataforge.meta.ObservableMutableMeta
import space.kscience.dataforge.meta.enum
import space.kscience.dataforge.meta.long
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.*
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionEvent
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.flowChanges
import space.kscience.visionforge.html.*
import kotlin.time.Duration.Companion.milliseconds
@ -72,7 +91,6 @@ public class VisionRoute(
/**
* Serve visions in a given [route] without providing a page template.
* [visions] could be changed during the service.
*
* @return a [Flow] of backward events, including vision change events
*/
@ -137,8 +155,8 @@ public fun Application.serveVisionData(
public fun Application.serveVisionData(
configuration: VisionRoute,
data: Map<Name, Vision>,
): Unit = serveVisionData(configuration) { data[it] }
data: Map<Name, VisionDisplay>,
): Unit = serveVisionData(configuration) { data[it]?.vision }
/**
* Serve a page, potentially containing any number of visions at a given [route] with given [header].
@ -154,10 +172,10 @@ public fun Application.visionPage(
) {
require(WebSockets)
val collector: MutableMap<Name, Vision> = mutableMapOf()
val cache: MutableMap<Name, VisionDisplay> = mutableMapOf()
//serve data
serveVisionData(configuration, collector)
serveVisionData(configuration, cache)
//filled pages
routing {
@ -193,7 +211,7 @@ public fun Application.visionPage(
path(route, "ws")
}
} else null,
onVisionRendered = { name, vision -> collector[name] = vision },
displayCache = cache,
fragment = visionFragment
)
}