New properties #34

Merged
altavir merged 19 commits from new-properties into dev 2020-12-29 13:35:01 +03:00
19 changed files with 211 additions and 140 deletions
Showing only changes of commit 56481933ed - Show all commits

View File

@ -17,5 +17,5 @@ fun main() {
}
}
fragment.makeFile(resourceLocation = ResourceLocation.LOCAL)
fragment.makeFile(resourceLocation = ResourceLocation.SYSTEM)
}

View File

@ -0,0 +1,37 @@
package hep.dataforge.vision.solid
import hep.dataforge.meta.DFExperimental
import hep.dataforge.vision.ResourceLocation
import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.html.fragment
import hep.dataforge.vision.three.server.makeFile
import hep.dataforge.vision.three.server.solid
import kotlinx.html.h1
import java.nio.file.Paths
import kotlin.random.Random
@OptIn(DFExperimental::class)
fun main() {
val random = Random(112233)
val fragment = VisionManager.fragment {
h1 { +"Happy new year!" }
vision {
solid {
repeat(100) {
sphere(5) {
x = random.nextDouble(-300.0, 300.0)
y = random.nextDouble(-300.0, 300.0)
z = random.nextDouble(-300.0, 300.0)
material {
color(random.nextInt())
specularColor(random.nextInt())
}
detail = 16
}
}
}
}
}
fragment.makeFile(Paths.get("stars.html"), resourceLocation = ResourceLocation.EMBED)
}

View File

@ -32,7 +32,7 @@ public external interface PropertyEditorProps : RProps {
public var provider: MutableItemProvider
/**
* Provide default item (greyed out if used
* Provide default item (greyed out if used)
*/
public var defaultProvider: ItemProvider?
@ -65,11 +65,12 @@ private val PropertyEditorItem: FunctionalComponent<PropertyEditorProps> =
private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
var expanded: Boolean by useState { true }
val itemName = useMemo( { props.name ?: Name.EMPTY }, arrayOf(props.name))
var item: MetaItem? by useState { props.provider.getItem(itemName) }
val itemName = props.name ?: Name.EMPTY
val descriptorItem: ItemDescriptor? =
useMemo({ props.descriptor?.get(itemName) }, arrayOf(props.descriptor, itemName))
var item: MetaItem? by useState { props.provider.getItem(itemName) }
if (descriptorItem?.hidden == true) return //fail fast for hidden property
var actualItem: MetaItem? by useState {

View File

@ -9,12 +9,10 @@ import hep.dataforge.vision.widgetType
import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onKeyDownFunction
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSelectElement
import org.w3c.dom.events.Event
import react.*
import react.dom.defaultValue
import react.dom.option
import styled.styledInput
import styled.styledSelect
@ -25,116 +23,142 @@ public external interface ValueChooserProps : RProps {
public var valueChanged: ((Value?) -> Unit)?
}
public external interface ValueChooserState : RState {
public var rawInput: Boolean?
@JsExport
public val StringValueChooser: FunctionalComponent<ValueChooserProps> =
functionalComponent("StringValueChooser") { props ->
var value by useState(props.item.string ?: "")
val keyDown: (Event) -> Unit = { event ->
if (event.type == "keydown" && event.asDynamic().key == "Enter") {
value = (event.target as HTMLInputElement).value
if(value!= props.item.string) {
props.valueChanged?.invoke(value.asValue())
}
}
}
val handleChange: (Event) -> Unit = {
value = (it.target as HTMLInputElement).value
}
styledInput(type = InputType.text) {
attrs {
this.value = value
onKeyDownFunction = keyDown
onChangeFunction = handleChange
}
}
}
@JsExport
public class ValueChooserComponent(props: ValueChooserProps) : RComponent<ValueChooserProps, ValueChooserState>(props) {
private val element = createRef<HTMLElement>()
private fun getValue(): Value? {
val element = element.current ?: return null//state.element ?: return null
return when (element) {
is HTMLInputElement -> if (element.type == "checkbox") {
if (element.checked) True else False
} else {
element.value.asValue()
public val BooleanValueChooser: FunctionalComponent<ValueChooserProps> =
functionalComponent("BooleanValueChooser") { props ->
var checkedValue by useState(props.item.boolean ?: false)
val handleChange: (Event) -> Unit = {
val newValue = (it.target as HTMLInputElement).checked
checkedValue = newValue
props.valueChanged?.invoke(newValue.asValue())
}
is HTMLSelectElement -> element.value.asValue()
else -> error("Unknown event target: $element")
}
}
private val commit: (Event) -> Unit = { _ ->
props.valueChanged?.invoke(getValue())
}
private val keyDown: (Event) -> Unit = { event ->
if (event.type == "keydown" && event.asDynamic().key == "Enter") {
commit(event)
}
}
override fun shouldComponentUpdate(
nextProps: ValueChooserProps,
nextState: ValueChooserState
): Boolean = nextProps.item !== props.item
override fun componentDidUpdate(prevProps: ValueChooserProps, prevState: ValueChooserState, snapshot: Any) {
(element.current as? HTMLInputElement)?.let { element ->
if (element.type == "checkbox") {
element.defaultChecked = props.item?.boolean ?: false
} else {
element.defaultValue = props.item?.string ?: ""
}
element.indeterminate = props.item == null
}
}
private fun RBuilder.stringInput() = styledInput(type = InputType.text) {
attrs {
this.defaultValue = props.item?.string ?: ""
onKeyDownFunction = keyDown
}
ref = element
}
override fun RBuilder.render() {
val descriptor = props.descriptor
val type = descriptor?.type?.firstOrNull()
when {
state.rawInput == true -> stringInput()
descriptor?.widgetType == "color" -> styledInput(type = InputType.color) {
ref = element
attrs {
this.defaultValue = props.item?.value?.let { value ->
if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int)
else value.string
} ?: "#000000"
onChangeFunction = commit
}
}
type == ValueType.BOOLEAN -> {
styledInput(type = InputType.checkBox) {
ref = element
attrs {
defaultChecked = props.item?.boolean ?: false
onChangeFunction = commit
this.attributes["indeterminate"] = (checkedValue == null).toString()
checked = checkedValue
onChangeFunction = handleChange
}
}
}
type == ValueType.NUMBER -> styledInput(type = InputType.number) {
ref = element
@JsExport
public val NumberValueChooser: FunctionalComponent<ValueChooserProps> =
functionalComponent("NumberValueChooser") { props ->
var value by useState(props.item.string ?: "")
val keyDown: (Event) -> Unit = { event ->
if (event.type == "keydown" && event.asDynamic().key == "Enter") {
value = (event.target as HTMLInputElement).value
val number = value.toDoubleOrNull()
if (number == null) {
console.error("The input value $value is not a number")
} else {
props.valueChanged?.invoke(number.asValue())
}
}
}
val handleChange: (Event) -> Unit = {
value = (it.target as HTMLInputElement).value
}
styledInput(type = InputType.number) {
attrs {
descriptor.attributes["step"].string?.let {
this.value = value
onKeyDownFunction = keyDown
onChangeFunction = handleChange
props.descriptor?.attributes?.get("step").string?.let {
step = it
}
descriptor.attributes["min"].string?.let {
props.descriptor?.attributes?.get("min").string?.let {
min = it
}
descriptor.attributes["max"].string?.let {
props.descriptor?.attributes?.get("max").string?.let {
max = it
}
defaultValue = props.item?.string ?: ""
onKeyDownFunction = keyDown
}
}
descriptor?.allowedValues?.isNotEmpty() ?: false -> styledSelect {
descriptor!!.allowedValues.forEach {
}
@JsExport
public val ComboValueChooser: FunctionalComponent<ValueChooserProps> =
functionalComponent("ComboValueChooser") { props ->
var selected by useState(props.item.string ?: "")
val handleChange: (Event) -> Unit = {
selected = (it.target as HTMLSelectElement).value
props.valueChanged?.invoke(selected.asValue())
}
styledSelect {
props.descriptor?.allowedValues?.forEach {
option {
+it.string
}
}
ref = element
attrs {
this.value = props.item?.string ?: ""
multiple = false
onChangeFunction = commit
onChangeFunction = handleChange
}
}
else -> stringInput()
}
@JsExport
public val ColorValueChooser: FunctionalComponent<ValueChooserProps> =
functionalComponent("ColorValueChooser") { props ->
var value by useState(
props.item.value?.let { value ->
if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int)
else value.string
} ?: "#000000"
)
val handleChange: (Event) -> Unit = {
value = (it.target as HTMLInputElement).value
props.valueChanged?.invoke(value.asValue())
}
styledInput(type = InputType.color) {
attrs {
this.value = value
onChangeFunction = handleChange
}
}
}
@JsExport
public val ValueChooser: FunctionalComponent<ValueChooserProps> = functionalComponent("ValueChooser") { props ->
val rawInput by useState(false)
val descriptor = props.descriptor
val type = descriptor?.type?.firstOrNull()
when {
rawInput -> child(StringValueChooser, props)
descriptor?.widgetType == "color" -> child(ColorValueChooser, props)
type == ValueType.BOOLEAN -> child(BooleanValueChooser, props)
type == ValueType.NUMBER -> child(NumberValueChooser, props)
descriptor?.allowedValues?.isNotEmpty() ?: false -> child(ComboValueChooser, props)
//TODO handle lists
else -> child(StringValueChooser, props)
}
}
@ -142,9 +166,9 @@ internal fun RBuilder.valueChooser(
name: Name,
item: MetaItem?,
descriptor: ValueDescriptor? = null,
callback: (Value?) -> Unit
callback: (Value?) -> Unit,
) {
child(ValueChooserComponent::class) {
child(ValueChooser) {
attrs {
key = name.toString()
this.item = item

View File

@ -1,5 +1,6 @@
package hep.dataforge.vision
import hep.dataforge.meta.DFExperimental
import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaItem
import hep.dataforge.meta.MutableItemProvider
@ -73,6 +74,7 @@ public interface Vision : Described {
* Flow of property invalidation events. It does not contain property values after invalidation since it is not clear
* if it should include inherited properties etc.
*/
@DFExperimental
@OptIn(ExperimentalCoroutinesApi::class)
public val propertyChanges: Flow<Name>
get() = callbackFlow<Name> {

View File

@ -73,7 +73,7 @@ public abstract class VisionTagConsumer<R>(
@OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision(
name: String,
name: String = DEFAULT_VISION_NAME,
visionProvider: VisionOutput.() -> Vision,
): T = vision(name.toName(), visionProvider)
@ -103,5 +103,7 @@ public abstract class VisionTagConsumer<R>(
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 = "."
public const val DEFAULT_VISION_NAME = "vision"
}
}

View File

@ -18,9 +18,10 @@ public fun FlowContent.embedVisionFragment(
val consumer = object : VisionTagConsumer<Any?>(consumer, idPrefix) {
override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) {
script {
type = "text/json"
attributes["class"] = OUTPUT_DATA_CLASS
unsafe {
+manager.encodeToString(vision)
+"\n${manager.encodeToString(vision)}\n"
}
}
}

View File

@ -35,7 +35,7 @@ public enum class ResourceLocation {
EMBED
}
internal const val DATAFORGE_ASSETS_PATH = ".dataforge/assets"
internal const val VISIONFORGE_ASSETS_PATH = ".dataforge/vision/assets"
/**
@ -43,14 +43,14 @@ internal const val DATAFORGE_ASSETS_PATH = ".dataforge/assets"
* @param
*/
internal fun checkOrStoreFile(basePath: Path, filePath: Path, resource: String): Path {
val fullPath = basePath.resolveSibling(filePath).toAbsolutePath()
val fullPath = basePath.resolveSibling(filePath).toAbsolutePath().resolve(resource)
if (Files.exists(fullPath)) {
//TODO checksum
} else {
//TODO add logging
val bytes = VisionManager::class.java.getResourceAsStream(resource).readAllBytes()
val bytes = VisionManager::class.java.getResourceAsStream("/$resource").readAllBytes()
Files.createDirectories(fullPath.parent)
Files.write(fullPath, bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
}
@ -58,7 +58,7 @@ internal fun checkOrStoreFile(basePath: Path, filePath: Path, resource: String):
return if (basePath.isAbsolute && fullPath.startsWith(basePath)) {
basePath.relativize(fullPath)
} else {
filePath
fullPath
}
}
@ -78,7 +78,7 @@ internal fun embedScriptHeader(resource: String): HtmlFragment = {
script {
type = "text/javascript"
unsafe {
val bytes = VisionManager::class.java.getResourceAsStream(resource).readAllBytes()
val bytes = VisionManager::class.java.getResourceAsStream("/$resource").readAllBytes()
+bytes.toString(Charsets.UTF_8)
}
}
@ -108,12 +108,12 @@ public fun Context.Companion.scriptHeader(
val targetPath = when (resourceLocation) {
ResourceLocation.LOCAL -> checkOrStoreFile(
basePath,
Path.of(DATAFORGE_ASSETS_PATH),
Path.of(VISIONFORGE_ASSETS_PATH),
scriptResource
)
ResourceLocation.SYSTEM -> checkOrStoreFile(
Path.of("."),
Path.of(System.getProperty("user.home")).resolve(DATAFORGE_ASSETS_PATH),
Path.of(System.getProperty("user.home")).resolve(VISIONFORGE_ASSETS_PATH),
scriptResource
)
ResourceLocation.EMBED -> null

View File

@ -20,8 +20,10 @@ public fun HtmlVisionFragment.makeFile(
title: String = "VisionForge page",
show: Boolean = true,
) {
val actualFile = path ?: Files.createTempFile("tempPlot", ".html")
Files.createDirectories(actualFile.parent)
val actualFile = path?.let {
Path.of(System.getProperty("user.home")).resolve(path)
} ?: Files.createTempFile("tempPlot", ".html")
//Files.createDirectories(actualFile.parent)
val htmlString = createHTML().apply {
head {
meta {

View File

@ -130,7 +130,7 @@ class FX3DPlugin : AbstractPlugin() {
}
companion object : PluginFactory<FX3DPlugin> {
override val tag = PluginTag("visual.fx3D", PluginTag.DATAFORGE_GROUP)
override val tag = PluginTag("vision.fx3D", PluginTag.DATAFORGE_GROUP)
override val type = FX3DPlugin::class
override fun invoke(meta: Meta, context: Context) = FX3DPlugin()
}

View File

@ -44,13 +44,13 @@ public interface Solid : Vision {
public val Y_POSITION_KEY: Name = POSITION_KEY + Y_KEY
public val Z_POSITION_KEY: Name = POSITION_KEY + Z_KEY
public val ROTATION: Name = "rotation".asName()
public val ROTATION_KEY: Name = "rotation".asName()
public val X_ROTATION_KEY: Name = ROTATION + X_KEY
public val Y_ROTATION_KEY: Name = ROTATION + Y_KEY
public val Z_ROTATION_KEY: Name = ROTATION + Z_KEY
public val X_ROTATION_KEY: Name = ROTATION_KEY + X_KEY
public val Y_ROTATION_KEY: Name = ROTATION_KEY + Y_KEY
public val Z_ROTATION_KEY: Name = ROTATION_KEY + Z_KEY
public val ROTATION_ORDER_KEY: Name = ROTATION + "order"
public val ROTATION_ORDER_KEY: Name = ROTATION_KEY + "order"
public val SCALE_KEY: Name = "scale".asName()

View File

@ -35,6 +35,6 @@ internal fun Meta.toVector(default: Float = 0f) = Point3D(
internal fun Solid.updatePosition(meta: Meta?) {
meta[Solid.POSITION_KEY].node?.toVector()?.let { position = it }
meta[Solid.ROTATION].node?.toVector()?.let { rotation = it }
meta[Solid.ROTATION_KEY].node?.toVector()?.let { rotation = it }
meta[Solid.SCALE_KEY].node?.toVector(1f)?.let { scale = it }
}

View File

@ -33,7 +33,7 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) {
}
public companion object : PluginFactory<SolidManager> {
override val tag: PluginTag = PluginTag(name = "visual.spatial", group = PluginTag.DATAFORGE_GROUP)
override val tag: PluginTag = PluginTag(name = "vision.solid", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out SolidManager> = SolidManager::class
override fun invoke(meta: Meta, context: Context): SolidManager = SolidManager(meta)

View File

@ -19,12 +19,12 @@ public class SolidMaterial : Scheme() {
/**
* Primary web-color for the material
*/
public var color: ColorAccessor = ColorAccessor(this, COLOR_KEY)
public val color: ColorAccessor = ColorAccessor(this, COLOR_KEY)
/**
* Specular color for phong material
*/
public var specularColor: ColorAccessor = ColorAccessor(this, SPECULAR_COLOR_KEY)
public val specularColor: ColorAccessor = ColorAccessor(this, SPECULAR_COLOR_KEY)
/**
* Opacity

View File

@ -1,9 +1,12 @@
package hep.dataforge.vision.solid.specifications
import hep.dataforge.meta.*
import hep.dataforge.meta.Scheme
import hep.dataforge.meta.SchemeSpec
import hep.dataforge.meta.boolean
import hep.dataforge.meta.double
public class Axes : Scheme() {
public var visible: Boolean by boolean(rootNode?.isEmpty() != false)
public var visible: Boolean by boolean(false)
public var size: Double by double(AXIS_SIZE)
public var width: Double by double(AXIS_WIDTH)

View File

@ -20,6 +20,7 @@ import info.laht.threekt.external.controls.OrbitControls
import info.laht.threekt.external.controls.TrackballControls
import info.laht.threekt.geometries.EdgesGeometry
import info.laht.threekt.helpers.AxesHelper
import info.laht.threekt.lights.AmbientLight
import info.laht.threekt.materials.LineBasicMaterial
import info.laht.threekt.math.Vector2
import info.laht.threekt.objects.LineSegments
@ -50,8 +51,11 @@ public class ThreeCanvas(
public var axes: AxesHelper = AxesHelper(options.axes.size.toInt()).apply { visible = options.axes.visible }
private set
private val light = AmbientLight(0x404040)
private val scene: Scene = Scene().apply {
add(axes)
add(light)
}
public var camera: PerspectiveCamera = buildCamera(options.camera)
@ -66,6 +70,7 @@ public class ThreeCanvas(
}
private val canvas = (renderer.domElement as HTMLCanvasElement).apply {
className += "three-canvas"
width = 600
height = 600
style.apply {
@ -131,7 +136,7 @@ public class ThreeCanvas(
}
internal fun attach(element: Element) {
check(element.children.length == 0){"The element for Three canvas is not empty"}
check(element.getElementsByClassName("three-canvas").length == 0){"Three canvas already created in this element"}
element.appendChild(canvas)
updateSize()
}

View File

@ -49,7 +49,7 @@ public fun Object3D.updateProperty(source: Vision, propertyName: Name) {
updateMaterialProperty(source, propertyName)
} else if (
propertyName.startsWith(Solid.POSITION_KEY)
|| propertyName.startsWith(Solid.ROTATION)
|| propertyName.startsWith(Solid.ROTATION_KEY)
|| propertyName.startsWith(Solid.SCALE_KEY)
) {
//update position of mesh using this object

View File

@ -70,7 +70,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
obj.onPropertyChange(updateScope) { name ->
if (
name.startsWith(Solid.POSITION_KEY) ||
name.startsWith(Solid.ROTATION) ||
name.startsWith(Solid.ROTATION_KEY) ||
name.startsWith(Solid.SCALE_KEY)
) {
//update position of mesh using this object
@ -81,14 +81,6 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
}
obj.structureChanges.onEach { (nameToken, _, child) ->
// if (name.isEmpty()) {
// logger.error { "Children change with empty name on $group" }
// return@onChildrenChange
// }
// val parentName = name.cutLast()
// val childName = name.last()!!
//removing old object
findChild(nameToken.asName())?.let { oldChild ->
oldChild.parent?.remove(oldChild)
@ -153,7 +145,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
}
public companion object : PluginFactory<ThreePlugin> {
override val tag: PluginTag = PluginTag("visual.three", PluginTag.DATAFORGE_GROUP)
override val tag: PluginTag = PluginTag("vision.threejs", PluginTag.DATAFORGE_GROUP)
override val type: KClass<ThreePlugin> = ThreePlugin::class
override fun invoke(meta: Meta, context: Context): ThreePlugin = ThreePlugin()
}

View File

@ -39,7 +39,9 @@ public fun HtmlVisionFragment.makeFile(
resourceLocation: ResourceLocation = ResourceLocation.SYSTEM,
show: Boolean = true,
) {
val actualPath = path ?: Files.createTempFile("tempPlot", ".html")
val scriptHeader = Context.scriptHeader("/js/visionforge-three.js", actualPath, resourceLocation)
val actualPath = path?.let {
Path.of(System.getProperty("user.home")).resolve(path)
} ?: Files.createTempFile("tempPlot", ".html")
val scriptHeader = Context.scriptHeader("js/visionforge-three.js", actualPath, resourceLocation)
makeFile(visionManager, path = path, show = show, title = title, headers = arrayOf(scriptHeader))
}