forked from kscience/visionforge
Working Server server provider.
This commit is contained in:
parent
9b42d4f186
commit
a85cd828e6
@ -1,20 +1,51 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("ru.mipt.npm.mpp")
|
id("ru.mipt.npm.mpp")
|
||||||
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
val kvisionVersion: String = "3.16.2"
|
|
||||||
|
group = "ru.mipt.npm"
|
||||||
|
|
||||||
|
//val kvisionVersion: String = "3.16.2"
|
||||||
|
|
||||||
kscience{
|
kscience{
|
||||||
|
useSerialization{
|
||||||
|
json()
|
||||||
|
}
|
||||||
application()
|
application()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
afterEvaluate {
|
||||||
|
val jsBrowserDistribution by tasks.getting
|
||||||
|
|
||||||
|
jvm {
|
||||||
|
withJava()
|
||||||
|
compilations[org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME]?.apply {
|
||||||
|
tasks.getByName<ProcessResources>(processResourcesTaskName) {
|
||||||
|
dependsOn(jsBrowserDistribution)
|
||||||
|
afterEvaluate {
|
||||||
|
from(jsBrowserDistribution)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":visionforge-solid"))
|
implementation(project(":visionforge-solid"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":visionforge-server"))
|
||||||
|
}
|
||||||
|
}
|
||||||
jsMain {
|
jsMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":visionforge-threejs"))
|
implementation(project(":visionforge-threejs"))
|
||||||
@ -22,3 +53,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("ru.mipt.npm.sat.SatServerKt")
|
||||||
|
}
|
||||||
|
@ -1,34 +1,40 @@
|
|||||||
package ru.mipt.npm.sat
|
package ru.mipt.npm.sat
|
||||||
|
|
||||||
import hep.dataforge.Application
|
|
||||||
import hep.dataforge.context.Global
|
import hep.dataforge.context.Global
|
||||||
import hep.dataforge.meta.invoke
|
import hep.dataforge.vision.client.VisionClient
|
||||||
import hep.dataforge.startApplication
|
import hep.dataforge.vision.client.fetchAndRenderAllVisions
|
||||||
import hep.dataforge.vision.solid.three.ThreePlugin
|
import hep.dataforge.vision.solid.three.ThreePlugin
|
||||||
import hep.dataforge.vision.solid.three.render
|
import kotlinx.browser.window
|
||||||
import kotlinx.browser.document
|
|
||||||
import org.w3c.dom.HTMLElement
|
|
||||||
|
|
||||||
private class SatDemoApp : Application {
|
//private class SatDemoApp : Application {
|
||||||
|
//
|
||||||
override fun start(state: Map<String, Any>) {
|
// override fun start(state: Map<String, Any>) {
|
||||||
val element = document.getElementById("canvas") as? HTMLElement
|
// val element = document.getElementById("canvas") as? HTMLElement
|
||||||
?: error("Element with id 'canvas' not found on page")
|
// ?: error("Element with id 'canvas' not found on page")
|
||||||
val three = Global.plugins.fetch(ThreePlugin)
|
// val three = Global.plugins.fetch(ThreePlugin)
|
||||||
val sat = visionOfSatellite(
|
//
|
||||||
ySegments = 3,
|
// val sat = visionOfSatellite(
|
||||||
)
|
// ySegments = 3,
|
||||||
three.render(element, sat){
|
// )
|
||||||
minSize = 500
|
// three.render(element, sat){
|
||||||
axes{
|
// minSize = 500
|
||||||
size = 500.0
|
// axes{
|
||||||
visible = true
|
// size = 500.0
|
||||||
}
|
// visible = true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
}
|
//
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//fun main() {
|
||||||
|
// startApplication(::SatDemoApp)
|
||||||
|
//}
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
startApplication(::SatDemoApp)
|
//Loading three-js renderer
|
||||||
|
Global.plugins.load(ThreePlugin)
|
||||||
|
window.onload = {
|
||||||
|
Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions()
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package ru.mipt.npm.sat
|
||||||
|
|
||||||
|
|
||||||
|
import hep.dataforge.context.Global
|
||||||
|
import hep.dataforge.names.asName
|
||||||
|
import hep.dataforge.vision.server.visionModule
|
||||||
|
import hep.dataforge.vision.solid.SolidManager
|
||||||
|
import io.ktor.server.cio.CIO
|
||||||
|
import io.ktor.server.engine.embeddedServer
|
||||||
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
|
import kotlinx.html.script
|
||||||
|
|
||||||
|
@OptIn(KtorExperimentalAPI::class)
|
||||||
|
fun main() {
|
||||||
|
val sat = visionOfSatellite(
|
||||||
|
ySegments = 3,
|
||||||
|
)
|
||||||
|
|
||||||
|
val context = Global.context("SAT"){
|
||||||
|
plugin(SolidManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedServer(CIO, 8080, host = "localhost"){
|
||||||
|
visionModule(context).apply {
|
||||||
|
header {
|
||||||
|
script {
|
||||||
|
src = "sat-demo.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page {
|
||||||
|
vision("main".asName(), sat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start(wait = true)
|
||||||
|
}
|
@ -12,16 +12,30 @@ repositories{
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
jvm()
|
||||||
js(IR) {
|
js(IR) {
|
||||||
browser {}
|
browser {
|
||||||
|
}
|
||||||
|
binaries.executable()
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":visionforge-solid"))
|
implementation(project(":visionforge-solid"))
|
||||||
api(project(":visionforge-gdml"))
|
implementation(project(":visionforge-gdml"))
|
||||||
api(project(":ui:bootstrap"))
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jsMain by getting{
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":ui:bootstrap"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jvmMain by getting{
|
||||||
|
dependencies {
|
||||||
|
implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,22 @@
|
|||||||
//package hep.dataforge.vision.solid
|
package hep.dataforge.vision.solid
|
||||||
|
|
||||||
|
import com.github.ricky12awesome.jss.encodeToSchema
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val schema = Json {
|
||||||
|
serializersModule = SolidManager.serializersModuleForSolids
|
||||||
|
prettyPrintIndent = " "
|
||||||
|
prettyPrint = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
isLenient = true
|
||||||
|
coerceInputValues = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}.encodeToSchema(SolidGroup.serializer(), generateDefinitions = false)
|
||||||
|
println(schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
//import hep.dataforge.meta.JSON_PRETTY
|
//import hep.dataforge.meta.JSON_PRETTY
|
||||||
//import kotlinx.serialization.*
|
//import kotlinx.serialization.*
|
@ -54,6 +54,7 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
|
|||||||
|
|
||||||
private val defaultSerialModule: SerializersModule = SerializersModule {
|
private val defaultSerialModule: SerializersModule = SerializersModule {
|
||||||
polymorphic(Vision::class) {
|
polymorphic(Vision::class) {
|
||||||
|
default { VisionBase.serializer() }
|
||||||
subclass(VisionBase.serializer())
|
subclass(VisionBase.serializer())
|
||||||
subclass(VisionGroupBase.serializer())
|
subclass(VisionGroupBase.serializer())
|
||||||
}
|
}
|
||||||
|
@ -63,5 +63,7 @@ public abstract class HtmlOutputScope<R, V : Vision>(
|
|||||||
public companion object {
|
public companion object {
|
||||||
public const val OUTPUT_CLASS: String = "visionforge-output"
|
public const val OUTPUT_CLASS: String = "visionforge-output"
|
||||||
public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name"
|
public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name"
|
||||||
|
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
|
||||||
|
public const val DEFAULT_ENDPOINT: String = "."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,43 +0,0 @@
|
|||||||
package hep.dataforge.vision.client
|
|
||||||
|
|
||||||
import hep.dataforge.context.Context
|
|
||||||
import hep.dataforge.context.Global
|
|
||||||
import hep.dataforge.vision.VisionManager
|
|
||||||
import hep.dataforge.vision.html.HtmlOutputScope
|
|
||||||
import kotlinx.browser.window
|
|
||||||
import org.w3c.dom.Element
|
|
||||||
import org.w3c.dom.get
|
|
||||||
import org.w3c.dom.url.URL
|
|
||||||
|
|
||||||
@JsExport
|
|
||||||
public class ClientVisionManager {
|
|
||||||
private val visionForgeContext: Context = Global.context("client") {
|
|
||||||
plugin(VisionManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val visionManager: VisionManager = visionForgeContext.plugins.fetch(VisionManager)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Up-going tree traversal in search for endpoint attribute
|
|
||||||
*/
|
|
||||||
private fun resolveEndpoint(element: Element?): String {
|
|
||||||
if(element == null) return DEFAULT_ENDPOINT
|
|
||||||
val attribute = element.attributes[OUTPUT_ENDPOINT_ATTRIBUTE]
|
|
||||||
return attribute?.value ?: resolveEndpoint(element.parentElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun renderVision(element: Element){
|
|
||||||
if(!element.classList.contains(HtmlOutputScope.OUTPUT_CLASS)) error("The element $element is not an output element")
|
|
||||||
val endpoint = URL(resolveEndpoint(element))
|
|
||||||
window.fetch("$endpoint/vision").then {response->
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public companion object {
|
|
||||||
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
|
|
||||||
public const val DEFAULT_ENDPOINT: String = ".."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
|||||||
|
package hep.dataforge.vision.client
|
||||||
|
|
||||||
|
import hep.dataforge.context.*
|
||||||
|
import hep.dataforge.meta.Meta
|
||||||
|
import hep.dataforge.vision.Vision
|
||||||
|
import hep.dataforge.vision.VisionManager
|
||||||
|
import hep.dataforge.vision.html.HtmlOutputScope
|
||||||
|
import hep.dataforge.vision.html.HtmlOutputScope.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
|
||||||
|
import hep.dataforge.vision.html.HtmlOutputScope.Companion.OUTPUT_NAME_ATTRIBUTE
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import kotlinx.browser.window
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.WebSocket
|
||||||
|
import org.w3c.dom.asList
|
||||||
|
import org.w3c.dom.get
|
||||||
|
import org.w3c.dom.url.URL
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
public class VisionClient : AbstractPlugin() {
|
||||||
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
private val visionManager: VisionManager by require(VisionManager)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Up-going tree traversal in search for endpoint attribute
|
||||||
|
*/
|
||||||
|
private fun resolveEndpoint(element: Element?): String {
|
||||||
|
if (element == null) return window.location.href
|
||||||
|
val attribute = element.attributes[OUTPUT_ENDPOINT_ATTRIBUTE]
|
||||||
|
return attribute?.value ?: resolveEndpoint(element.parentElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveName(element: Element): String? {
|
||||||
|
val attribute = element.attributes[OUTPUT_NAME_ATTRIBUTE]
|
||||||
|
return attribute?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRenderers() = context.gather<ElementVisionRenderer>(ElementVisionRenderer.TYPE).values
|
||||||
|
|
||||||
|
public fun findRendererFor(vision: Vision): ElementVisionRenderer? = getRenderers().maxByOrNull { it.rateVision(vision) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch from server and render a vision, described in a given with [HtmlOutputScope.OUTPUT_CLASS] class.
|
||||||
|
*/
|
||||||
|
public fun fetchAndRenderVision(element: Element, requestUpdates: Boolean = true) {
|
||||||
|
val name = resolveName(element) ?: error("The element is not a vision output")
|
||||||
|
console.info("Found DF output with name $name")
|
||||||
|
if (!element.classList.contains(HtmlOutputScope.OUTPUT_CLASS)) error("The element $element is not an output element")
|
||||||
|
val endpoint = resolveEndpoint(element)
|
||||||
|
console.info("Vision server is resolved to $endpoint")
|
||||||
|
val fetchUrl = URL(endpoint).apply {
|
||||||
|
searchParams.append("name", name)
|
||||||
|
pathname += "/vision"
|
||||||
|
}
|
||||||
|
window.fetch(fetchUrl).then { response ->
|
||||||
|
if (response.ok) {
|
||||||
|
response.text().then { text ->
|
||||||
|
val vision = visionManager.jsonFormat.decodeFromString(Vision.serializer(), text)
|
||||||
|
|
||||||
|
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
|
||||||
|
renderer.render(element, vision)
|
||||||
|
if (requestUpdates) {
|
||||||
|
val wsUrl = URL(endpoint).apply {
|
||||||
|
pathname += "/ws"
|
||||||
|
protocol = "ws"
|
||||||
|
searchParams.append("name", name)
|
||||||
|
}
|
||||||
|
val ws = WebSocket(wsUrl.toString()).apply {
|
||||||
|
onmessage = { messageEvent ->
|
||||||
|
val stringData: String? = messageEvent.data as? String
|
||||||
|
if (stringData != null) {
|
||||||
|
val update = visionManager.jsonFormat.decodeFromString(Vision.serializer(), text)
|
||||||
|
vision.update(update)
|
||||||
|
} else {
|
||||||
|
console.error("WebSocket message data is not a string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onopen = {
|
||||||
|
console.info("WebSocket update channel established for output '$name'")
|
||||||
|
}
|
||||||
|
onclose = {
|
||||||
|
console.info("WebSocket update channel closed for output '$name'")
|
||||||
|
}
|
||||||
|
onerror = {
|
||||||
|
console.error("WebSocket update channel error for output '$name'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch initial vision state from $endpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object : PluginFactory<VisionClient> {
|
||||||
|
|
||||||
|
override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient()
|
||||||
|
|
||||||
|
override val tag: PluginTag = PluginTag(name = "vision.client", group = PluginTag.DATAFORGE_GROUP)
|
||||||
|
|
||||||
|
override val type: KClass<out VisionClient> = VisionClient::class
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and render visions for all elements with [HtmlOutputScope.OUTPUT_CLASS] class inside given [element].
|
||||||
|
*/
|
||||||
|
public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates: Boolean = true) {
|
||||||
|
val elements = element.getElementsByClassName(HtmlOutputScope.OUTPUT_CLASS)
|
||||||
|
console.info("Finished search for outputs. Found ${elements.length} items")
|
||||||
|
elements.asList().forEach { child ->
|
||||||
|
fetchAndRenderVision(child, requestUpdates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch visions from the server for all elements with [HtmlOutputScope.OUTPUT_CLASS] class in the document body
|
||||||
|
*/
|
||||||
|
public fun VisionClient.fetchAndRenderAllVisions(requestUpdates: Boolean = true){
|
||||||
|
val element = document.body ?: error("Document does not have a body")
|
||||||
|
fetchVisionsInChildren(element, requestUpdates)
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
package hep.dataforge.vision.client
|
package hep.dataforge.vision.client
|
||||||
|
|
||||||
|
import hep.dataforge.meta.DFExperimental
|
||||||
import hep.dataforge.names.Name
|
import hep.dataforge.names.Name
|
||||||
import hep.dataforge.names.toName
|
import hep.dataforge.names.toName
|
||||||
|
import hep.dataforge.provider.Type
|
||||||
import hep.dataforge.vision.Vision
|
import hep.dataforge.vision.Vision
|
||||||
import hep.dataforge.vision.html.BindingHtmlOutputScope
|
import hep.dataforge.vision.html.BindingHtmlOutputScope
|
||||||
import hep.dataforge.vision.html.HtmlOutputScope
|
import hep.dataforge.vision.html.HtmlOutputScope
|
||||||
@ -10,18 +12,38 @@ import kotlinx.browser.document
|
|||||||
import kotlinx.html.TagConsumer
|
import kotlinx.html.TagConsumer
|
||||||
import org.w3c.dom.*
|
import org.w3c.dom.*
|
||||||
|
|
||||||
public interface ElementVisionRenderer<in V : Vision> {
|
@Type(ElementVisionRenderer.TYPE)
|
||||||
public fun render(element: Element, vision: V): Unit
|
public interface ElementVisionRenderer {
|
||||||
}
|
|
||||||
|
|
||||||
public fun <V : Vision> Map<String, V>.bind(renderer: ElementVisionRenderer<V>) {
|
/**
|
||||||
forEach { (id, vision) ->
|
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
|
||||||
val element = document.getElementById(id) ?: error("Could not find element with id $id")
|
* can't process a vision. The value of [DEFAULT_RATING] used for default renderer. Specialized renderers could specify
|
||||||
renderer.render(element, vision)
|
* higher value in order to "steal" rendering job
|
||||||
|
*/
|
||||||
|
public fun rateVision(vision: Vision): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the [vision] inside a given [element] replacing its current content
|
||||||
|
*/
|
||||||
|
public fun render(element: Element, vision: Vision): Unit
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
public const val TYPE: String = "elementVisionRenderer"
|
||||||
|
public const val ZERO_RATING: Int = 0
|
||||||
|
public const val DEFAULT_RATING: Int = 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <V : Vision> Element.renderVisions(renderer: ElementVisionRenderer<V>, visionProvider: (Name) -> V?) {
|
@DFExperimental
|
||||||
|
public fun Map<String, Vision>.bind(rendererFactory: (Vision) -> ElementVisionRenderer) {
|
||||||
|
forEach { (id, vision) ->
|
||||||
|
val element = document.getElementById(id) ?: error("Could not find element with id $id")
|
||||||
|
rendererFactory(vision).render(element, vision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DFExperimental
|
||||||
|
public fun Element.renderAllVisions(visionProvider: (Name) -> Vision, rendererFactory: (Vision) -> ElementVisionRenderer) {
|
||||||
val elements = getElementsByClassName(HtmlOutputScope.OUTPUT_CLASS)
|
val elements = getElementsByClassName(HtmlOutputScope.OUTPUT_CLASS)
|
||||||
elements.asList().forEach { element ->
|
elements.asList().forEach { element ->
|
||||||
val name = element.attributes[HtmlOutputScope.OUTPUT_NAME_ATTRIBUTE]?.value
|
val name = element.attributes[HtmlOutputScope.OUTPUT_NAME_ATTRIBUTE]?.value
|
||||||
@ -30,21 +52,19 @@ public fun <V : Vision> Element.renderVisions(renderer: ElementVisionRenderer<V>
|
|||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
val vision = visionProvider(name.toName())
|
val vision = visionProvider(name.toName())
|
||||||
if (vision == null) {
|
rendererFactory(vision).render(element, vision)
|
||||||
console.error("Vision with name $name is not resolved")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
renderer.render(element, vision)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <V : Vision> Document.renderVisions(renderer: ElementVisionRenderer<V>, visionProvider: (Name) -> V?): Unit {
|
@DFExperimental
|
||||||
documentElement?.renderVisions(renderer, visionProvider)
|
public fun Document.renderAllVisions(visionProvider: (Name) -> Vision, rendererFactory: (Vision) -> ElementVisionRenderer): Unit {
|
||||||
|
documentElement?.renderAllVisions(visionProvider,rendererFactory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DFExperimental
|
||||||
public fun HtmlVisionFragment<Vision>.renderInDocument(
|
public fun HtmlVisionFragment<Vision>.renderInDocument(
|
||||||
root: TagConsumer<HTMLElement>,
|
root: TagConsumer<HTMLElement>,
|
||||||
renderer: ElementVisionRenderer<Vision>,
|
renderer: ElementVisionRenderer,
|
||||||
): HTMLElement = BindingHtmlOutputScope<HTMLElement, Vision>(root).apply(content).let { scope ->
|
): HTMLElement = BindingHtmlOutputScope<HTMLElement, Vision>(root).apply(content).let { scope ->
|
||||||
scope.finalize().apply {
|
scope.finalize().apply {
|
||||||
scope.bindings.forEach { (name, vision) ->
|
scope.bindings.forEach { (name, vision) ->
|
||||||
|
@ -14,6 +14,7 @@ import hep.dataforge.vision.html.*
|
|||||||
import hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE
|
import hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE
|
||||||
import io.ktor.application.*
|
import io.ktor.application.*
|
||||||
import io.ktor.features.CORS
|
import io.ktor.features.CORS
|
||||||
|
import io.ktor.features.CallLogging
|
||||||
import io.ktor.html.respondHtml
|
import io.ktor.html.respondHtml
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
@ -23,7 +24,10 @@ import io.ktor.http.content.static
|
|||||||
import io.ktor.http.withCharset
|
import io.ktor.http.withCharset
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.response.respondText
|
import io.ktor.response.respondText
|
||||||
import io.ktor.routing.*
|
import io.ktor.routing.application
|
||||||
|
import io.ktor.routing.get
|
||||||
|
import io.ktor.routing.route
|
||||||
|
import io.ktor.routing.routing
|
||||||
import io.ktor.server.engine.ApplicationEngine
|
import io.ktor.server.engine.ApplicationEngine
|
||||||
import io.ktor.websocket.WebSockets
|
import io.ktor.websocket.WebSockets
|
||||||
import io.ktor.websocket.webSocket
|
import io.ktor.websocket.webSocket
|
||||||
@ -34,9 +38,12 @@ import java.awt.Desktop
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.time.milliseconds
|
import kotlin.time.milliseconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ktor plugin container with given [routing]
|
||||||
|
*/
|
||||||
public class VisionServer internal constructor(
|
public class VisionServer internal constructor(
|
||||||
private val visionManager: VisionManager,
|
private val visionManager: VisionManager,
|
||||||
private val routing: Routing,
|
private val application: Application,
|
||||||
private val rootRoute: String,
|
private val rootRoute: String,
|
||||||
) : Configurable {
|
) : Configurable {
|
||||||
override val config: Config = Config()
|
override val config: Config = Config()
|
||||||
@ -57,7 +64,7 @@ public class VisionServer internal constructor(
|
|||||||
title: String,
|
title: String,
|
||||||
headers: List<HtmlFragment>,
|
headers: List<HtmlFragment>,
|
||||||
): Map<Name, Vision> {
|
): Map<Name, Vision> {
|
||||||
lateinit var result: Map<Name, Vision>
|
lateinit var visionMap: Map<Name, Vision>
|
||||||
|
|
||||||
head {
|
head {
|
||||||
meta {
|
meta {
|
||||||
@ -69,21 +76,26 @@ public class VisionServer internal constructor(
|
|||||||
title(title)
|
title(title)
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
result = visionFragment(visionFragment)
|
// attributes[OUTPUT_ENDPOINT_ATTRIBUTE] = if (rootRoute.endsWith("/")) {
|
||||||
script {
|
// rootRoute
|
||||||
type = "text/javascript"
|
// } else {
|
||||||
|
// "$rootRoute/"
|
||||||
val normalizedRoute = if (rootRoute.endsWith("/")) {
|
// }
|
||||||
rootRoute
|
//Load the fragment and remember all loaded visions
|
||||||
} else {
|
visionMap = visionFragment(visionFragment)
|
||||||
"$rootRoute/"
|
// //The script runs when all headers already with required libraries are already loaded
|
||||||
}
|
// script {
|
||||||
|
// type = "text/javascript"
|
||||||
src = TODO()//"${normalizedRoute}js/plotlyConnect.js"
|
//
|
||||||
}
|
// val normalizedRoute =
|
||||||
|
// unsafe {
|
||||||
|
// //language=JavaScript
|
||||||
|
// +"fetchAndRenderAllVisions()"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return visionMap
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun page(
|
public fun page(
|
||||||
@ -102,56 +114,50 @@ public class VisionServer internal constructor(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
routing.createRouteFromPath(rootRoute).apply {
|
application.routing {
|
||||||
route(route) {
|
route(rootRoute) {
|
||||||
//Update websocket
|
route(route) {
|
||||||
webSocket("ws") {
|
//Update websocket
|
||||||
val name: String = call.request.queryParameters["name"]
|
webSocket("ws") {
|
||||||
?: error("Vision name is not defined in parameters")
|
val name: String = call.request.queryParameters["name"]
|
||||||
|
?: error("Vision name is not defined in parameters")
|
||||||
|
|
||||||
application.log.debug("Opened server socket for $name")
|
application.log.debug("Opened server socket for $name")
|
||||||
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
|
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
|
||||||
try {
|
try {
|
||||||
vision.flowChanges(this, updateInterval.milliseconds).collect { update ->
|
vision.flowChanges(this, updateInterval.milliseconds).collect { update ->
|
||||||
val json = visionManager.encodeToString(update)
|
val json = visionManager.encodeToString(update)
|
||||||
outgoing.send(Frame.Text(json))
|
outgoing.send(Frame.Text(json))
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
application.log.debug("Closed server socket for $name")
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
|
||||||
application.log.debug("Closed server socket for $name")
|
|
||||||
}
|
}
|
||||||
}
|
//Plots in their json representation
|
||||||
//Plots in their json representation
|
get("vision") {
|
||||||
get("vision") {
|
val name: String = call.request.queryParameters["name"]
|
||||||
val name: String = call.request.queryParameters["name"]
|
?: error("Vision name is not defined in parameters")
|
||||||
?: error("Vision name is not defined in parameters")
|
|
||||||
|
|
||||||
val vision: Vision? = visions[name.toName()]
|
val vision: Vision? = visions[name.toName()]
|
||||||
if (vision == null) {
|
if (vision == null) {
|
||||||
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
|
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
|
||||||
} else {
|
} else {
|
||||||
call.respondText(
|
call.respondText(
|
||||||
visionManager.encodeToString(vision),
|
visionManager.encodeToString(vision),
|
||||||
contentType = ContentType.Application.Json,
|
contentType = ContentType.Application.Json,
|
||||||
status = HttpStatusCode.OK
|
status = HttpStatusCode.OK
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//filled pages
|
//filled pages
|
||||||
get {
|
get {
|
||||||
// val origin = call.request.origin
|
if (cachedHtml == null) {
|
||||||
// val url = URLBuilder().apply {
|
call.respondHtml {
|
||||||
// protocol = URLProtocol.createOrDefault(origin.scheme)
|
visions.putAll(buildPage(visionFragment, title, headers))
|
||||||
// //workaround for https://github.com/ktorio/ktor/issues/1663
|
}
|
||||||
// host = if (origin.host.startsWith("0:")) "[${origin.host}]" else origin.host
|
} else {
|
||||||
// port = origin.port
|
call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
|
||||||
// encodedPath = origin.uri
|
|
||||||
// }.build()
|
|
||||||
if (cachedHtml == null) {
|
|
||||||
call.respondHtml {
|
|
||||||
visions.putAll(buildPage(visionFragment, title, headers))
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,7 +166,7 @@ public class VisionServer internal constructor(
|
|||||||
|
|
||||||
public fun page(
|
public fun page(
|
||||||
route: String = DEFAULT_PAGE,
|
route: String = DEFAULT_PAGE,
|
||||||
title: String = "Plotly server page '$route'",
|
title: String = "VisionForge server page '$route'",
|
||||||
headers: List<HtmlFragment> = emptyList(),
|
headers: List<HtmlFragment> = emptyList(),
|
||||||
content: HtmlOutputScope<*, Vision>.() -> Unit,
|
content: HtmlOutputScope<*, Vision>.() -> Unit,
|
||||||
) {
|
) {
|
||||||
@ -170,7 +176,6 @@ public class VisionServer internal constructor(
|
|||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public const val DEFAULT_PAGE: String = "/"
|
public const val DEFAULT_PAGE: String = "/"
|
||||||
public val UPDATE_MODE_KEY: Name = "update.mode".toName()
|
|
||||||
public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName()
|
public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,8 +195,12 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(featureOrNull(CallLogging) == null){
|
||||||
|
install(CallLogging)
|
||||||
|
}
|
||||||
|
|
||||||
val routing = routing {
|
|
||||||
|
routing {
|
||||||
route(route) {
|
route(route) {
|
||||||
static {
|
static {
|
||||||
resources()
|
resources()
|
||||||
@ -201,7 +210,7 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA
|
|||||||
|
|
||||||
val visionManager = context.plugins.fetch(VisionManager)
|
val visionManager = context.plugins.fetch(VisionManager)
|
||||||
|
|
||||||
return VisionServer(visionManager, routing, route)
|
return VisionServer(visionManager, this, route)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun ApplicationEngine.show() {
|
public fun ApplicationEngine.show() {
|
||||||
|
@ -12,7 +12,7 @@ public class Box(
|
|||||||
public val xSize: Float,
|
public val xSize: Float,
|
||||||
public val ySize: Float,
|
public val ySize: Float,
|
||||||
public val zSize: Float
|
public val zSize: Float
|
||||||
) : BasicSolid(), GeometrySolid {
|
) : SolidBase(), GeometrySolid {
|
||||||
|
|
||||||
//TODO add helper for color configuration
|
//TODO add helper for color configuration
|
||||||
override fun <T : Any> toGeometry(geometryBuilder: GeometryBuilder<T>) {
|
override fun <T : Any> toGeometry(geometryBuilder: GeometryBuilder<T>) {
|
||||||
|
@ -19,7 +19,7 @@ public class Composite(
|
|||||||
public val compositeType: CompositeType,
|
public val compositeType: CompositeType,
|
||||||
public val first: Solid,
|
public val first: Solid,
|
||||||
public val second: Solid
|
public val second: Solid
|
||||||
) : BasicSolid(), Solid, VisionGroup {
|
) : SolidBase(), Solid, VisionGroup {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
first.parent = this
|
first.parent = this
|
||||||
|
@ -18,7 +18,7 @@ public class ConeSegment(
|
|||||||
public var upperRadius: Float,
|
public var upperRadius: Float,
|
||||||
public var startAngle: Float = 0f,
|
public var startAngle: Float = 0f,
|
||||||
public var angle: Float = PI2
|
public var angle: Float = PI2
|
||||||
) : BasicSolid(), GeometrySolid {
|
) : SolidBase(), GeometrySolid {
|
||||||
|
|
||||||
override fun <T : Any> toGeometry(geometryBuilder: GeometryBuilder<T>) {
|
override fun <T : Any> toGeometry(geometryBuilder: GeometryBuilder<T>) {
|
||||||
val segments = detail ?: 8
|
val segments = detail ?: 8
|
||||||
|
@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("solid.convex")
|
@SerialName("solid.convex")
|
||||||
public class Convex(public val points: List<Point3D>) : BasicSolid(), Solid
|
public class Convex(public val points: List<Point3D>) : SolidBase(), Solid
|
||||||
|
|
||||||
public inline fun VisionContainerBuilder<Solid>.convex(name: String = "", action: ConvexBuilder.() -> Unit = {}): Convex =
|
public inline fun VisionContainerBuilder<Solid>.convex(name: String = "", action: ConvexBuilder.() -> Unit = {}): Convex =
|
||||||
ConvexBuilder().apply(action).build().also { set(name, it) }
|
ConvexBuilder().apply(action).build().also { set(name, it) }
|
||||||
|
@ -39,7 +39,7 @@ public data class Layer(var x: Float, var y: Float, var z: Float, var scale: Flo
|
|||||||
public class Extruded(
|
public class Extruded(
|
||||||
public var shape: List<Point2D> = ArrayList(),
|
public var shape: List<Point2D> = ArrayList(),
|
||||||
public var layers: MutableList<Layer> = ArrayList()
|
public var layers: MutableList<Layer> = ArrayList()
|
||||||
) : BasicSolid(), GeometrySolid {
|
) : SolidBase(), GeometrySolid {
|
||||||
|
|
||||||
public fun shape(block: Shape2DBuilder.() -> Unit) {
|
public fun shape(block: Shape2DBuilder.() -> Unit) {
|
||||||
this.shape = Shape2DBuilder().apply(block).build()
|
this.shape = Shape2DBuilder().apply(block).build()
|
||||||
|
@ -12,7 +12,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("solid.line")
|
@SerialName("solid.line")
|
||||||
public class PolyLine(public var points: List<Point3D>) : BasicSolid(), Solid {
|
public class PolyLine(public var points: List<Point3D>) : SolidBase(), Solid {
|
||||||
|
|
||||||
//var lineType by string()
|
//var lineType by string()
|
||||||
public var thickness: Number by props().number(1.0, key = SolidMaterial.MATERIAL_KEY + THICKNESS_KEY)
|
public var thickness: Number by props().number(1.0, key = SolidMaterial.MATERIAL_KEY + THICKNESS_KEY)
|
||||||
|
@ -12,7 +12,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("solid")
|
@SerialName("solid")
|
||||||
public open class BasicSolid : VisionBase(), Solid {
|
public open class SolidBase : VisionBase(), Solid {
|
||||||
override val descriptor: NodeDescriptor get() = Solid.descriptor
|
override val descriptor: NodeDescriptor get() = Solid.descriptor
|
||||||
|
|
||||||
override var position: Point3D? = null
|
override var position: Point3D? = null
|
@ -11,7 +11,7 @@ public class SolidLabel(
|
|||||||
public var text: String,
|
public var text: String,
|
||||||
public var fontSize: Double,
|
public var fontSize: Double,
|
||||||
public var fontFamily: String,
|
public var fontFamily: String,
|
||||||
) : BasicSolid(), Solid
|
) : SolidBase(), Solid
|
||||||
|
|
||||||
public fun VisionContainerBuilder<Solid>.label(
|
public fun VisionContainerBuilder<Solid>.label(
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -7,10 +7,7 @@ import hep.dataforge.context.PluginTag
|
|||||||
import hep.dataforge.meta.Meta
|
import hep.dataforge.meta.Meta
|
||||||
import hep.dataforge.names.Name
|
import hep.dataforge.names.Name
|
||||||
import hep.dataforge.names.toName
|
import hep.dataforge.names.toName
|
||||||
import hep.dataforge.vision.Vision
|
import hep.dataforge.vision.*
|
||||||
import hep.dataforge.vision.VisionGroup
|
|
||||||
import hep.dataforge.vision.VisionGroupBase
|
|
||||||
import hep.dataforge.vision.VisionManager
|
|
||||||
import hep.dataforge.vision.VisionManager.Companion.VISION_SERIALIZER_MODULE_TARGET
|
import hep.dataforge.vision.VisionManager.Companion.VISION_SERIALIZER_MODULE_TARGET
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
import kotlinx.serialization.modules.PolymorphicModuleBuilder
|
||||||
@ -42,6 +39,7 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) {
|
|||||||
subclass(Composite.serializer())
|
subclass(Composite.serializer())
|
||||||
subclass(Tube.serializer())
|
subclass(Tube.serializer())
|
||||||
subclass(Box.serializer())
|
subclass(Box.serializer())
|
||||||
|
subclass(ConeSegment.serializer())
|
||||||
subclass(Convex.serializer())
|
subclass(Convex.serializer())
|
||||||
subclass(Extruded.serializer())
|
subclass(Extruded.serializer())
|
||||||
subclass(PolyLine.serializer())
|
subclass(PolyLine.serializer())
|
||||||
@ -51,11 +49,13 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) {
|
|||||||
|
|
||||||
public val serializersModuleForSolids: SerializersModule = SerializersModule {
|
public val serializersModuleForSolids: SerializersModule = SerializersModule {
|
||||||
polymorphic(Vision::class) {
|
polymorphic(Vision::class) {
|
||||||
|
subclass(VisionBase.serializer())
|
||||||
subclass(VisionGroupBase.serializer())
|
subclass(VisionGroupBase.serializer())
|
||||||
solids()
|
solids()
|
||||||
}
|
}
|
||||||
|
|
||||||
polymorphic(Solid::class) {
|
polymorphic(Solid::class) {
|
||||||
|
default { SolidBase.serializer() }
|
||||||
solids()
|
solids()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
|
||||||
public abstract class AbstractReference : BasicSolid(), VisionGroup {
|
public abstract class AbstractReference : SolidBase(), VisionGroup {
|
||||||
public abstract val prototype: Solid
|
public abstract val prototype: Solid
|
||||||
|
|
||||||
override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = sequence {
|
override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = sequence {
|
||||||
|
@ -16,7 +16,7 @@ public class Sphere(
|
|||||||
public var phi: Float = PI2,
|
public var phi: Float = PI2,
|
||||||
public var thetaStart: Float = 0f,
|
public var thetaStart: Float = 0f,
|
||||||
public var theta: Float = PI.toFloat(),
|
public var theta: Float = PI.toFloat(),
|
||||||
) : BasicSolid(), GeometrySolid {
|
) : SolidBase(), GeometrySolid {
|
||||||
|
|
||||||
override fun <T : Any> toGeometry(geometryBuilder: GeometryBuilder<T>) {
|
override fun <T : Any> toGeometry(geometryBuilder: GeometryBuilder<T>) {
|
||||||
fun point3DfromSphCoord(r: Float, theta: Float, phi: Float): Point3D {
|
fun point3DfromSphCoord(r: Float, theta: Float, phi: Float): Point3D {
|
||||||
|
@ -19,7 +19,7 @@ public class Tube(
|
|||||||
public var innerRadius: Float = 0f,
|
public var innerRadius: Float = 0f,
|
||||||
public var startAngle: Float = 0f,
|
public var startAngle: Float = 0f,
|
||||||
public var angle: Float = PI2,
|
public var angle: Float = PI2,
|
||||||
) : BasicSolid(), GeometrySolid {
|
) : SolidBase(), GeometrySolid {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(radius > 0)
|
require(radius > 0)
|
||||||
|
@ -15,7 +15,7 @@ import kotlin.collections.set
|
|||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import info.laht.threekt.objects.Group as ThreeGroup
|
import info.laht.threekt.objects.Group as ThreeGroup
|
||||||
|
|
||||||
public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer<Solid> {
|
public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
|
||||||
override val tag: PluginTag get() = Companion.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
public val solidManager: SolidManager by require(SolidManager)
|
public val solidManager: SolidManager by require(SolidManager)
|
||||||
@ -122,8 +122,19 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer<Solid> {
|
|||||||
attach(element)
|
attach(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun render(element: Element, vision: Solid) {
|
override fun content(target: String): Map<Name, Any> {
|
||||||
createCanvas(element).render(vision)
|
return when (target) {
|
||||||
|
ElementVisionRenderer.TYPE -> mapOf("three".asName() to this)
|
||||||
|
else -> super.content(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun rateVision(vision: Vision): Int {
|
||||||
|
return if (vision is Solid) ElementVisionRenderer.DEFAULT_RATING else ElementVisionRenderer.ZERO_RATING
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun render(element: Element, vision: Vision) {
|
||||||
|
createCanvas(element).render(vision as? Solid ?: error("Only solids are rendered"))
|
||||||
}
|
}
|
||||||
|
|
||||||
public companion object : PluginFactory<ThreePlugin> {
|
public companion object : PluginFactory<ThreePlugin> {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package hep.dataforge.vision.solid.three
|
package hep.dataforge.vision.solid.three
|
||||||
|
|
||||||
import hep.dataforge.vision.solid.BasicSolid
|
import hep.dataforge.vision.solid.SolidBase
|
||||||
import info.laht.threekt.core.Object3D
|
import info.laht.threekt.core.Object3D
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom visual object that has its own Three.js renderer
|
* A custom visual object that has its own Three.js renderer
|
||||||
*/
|
*/
|
||||||
public abstract class ThreeVision : BasicSolid() {
|
public abstract class ThreeVision : SolidBase() {
|
||||||
public abstract fun render(): Object3D
|
public abstract fun render(): Object3D
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user