Fix (almost) property resolution

This commit is contained in:
Alexander Nozik 2022-08-13 18:17:22 +03:00
parent ecf4a6a198
commit c586a2ea14
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
24 changed files with 123 additions and 64 deletions

View File

@ -49,7 +49,7 @@ fun main() {
}
vision("form") { form }
form.onPropertyChange {
form.onPropertyChange { _, _ ->
println(this)
}
}

View File

@ -59,6 +59,7 @@ internal class VariableBox(val xSize: Number, val ySize: Number) : ThreeJsVision
material.color.setRGB(r.toFloat() / 256, g.toFloat() / 256, b.toFloat() / 256)
mesh.updateMatrix()
}
name.startsWith(ThreeMeshFactory.EDGES_KEY) -> mesh.applyEdges(this@VariableBox)
else -> mesh.updateProperty(this@VariableBox, name)
}

View File

@ -74,6 +74,8 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
val descriptor: MetaDescriptor? = useMemo(props.descriptor, props.name) { props.descriptor?.get(props.name) }
var property: MutableMeta by useState { props.meta.getOrCreate(props.name) }
val defined = props.getPropertyState(props.name) == EditorPropertyState.Defined
val keys = useMemo(descriptor) {
buildSet {
descriptor?.children?.filterNot {
@ -134,7 +136,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
styledSpan {
css {
+TreeStyles.treeLabel
if (property.isEmpty()) {
if (!defined) {
+TreeStyles.treeLabelInactive
}
}
@ -175,7 +177,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
}
+"\u00D7"
attrs {
if (property.isEmpty()) {
if (!defined) {
disabled = true
} else {
onClickFunction = removeClick

View File

@ -1,5 +1,6 @@
package space.kscience.visionforge.ring
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.css.BorderStyle
import kotlinx.css.Color
@ -52,6 +53,7 @@ internal external interface CanvasControlsProps : Props {
public var vision: Vision?
}
@OptIn(DelicateCoroutinesApi::class)
internal val CanvasControls: FC<CanvasControlsProps> = fc("CanvasControls") { props ->
flexColumn {
flexRow {

View File

@ -18,10 +18,10 @@ private tailrec fun styleIsDefined(vision: Vision, reference: StyleReference): B
}
@VisionBuilder
public fun Vision.useStyle(reference: StyleReference) {
public fun Vision.useStyle(reference: StyleReference, notify: Boolean = true) {
//check that style is defined in a parent
//check(styleIsDefined(this, reference)) { "Style reference does not belong to a Vision parent" }
useStyle(reference.name)
useStyle(reference.name, notify)
}
@VisionBuilder

View File

@ -83,10 +83,12 @@ public var Vision.styles: List<String>
public val Vision.styleSheet: StyleSheet get() = StyleSheet(this)
/**
* Add style name to the list of styles to be resolved later. The style with given name does not necessary exist at the moment.
* Add style name to the list of styles to be resolved later.
* The style with given name does not necessary exist at the moment.
*/
public fun Vision.useStyle(name: String) {
styles = (properties.own?.get(Vision.STYLE_KEY)?.stringList ?: emptyList()) + name
public fun Vision.useStyle(name: String, notify: Boolean = true) {
val newStyle = properties.own?.get(Vision.STYLE_KEY)?.value?.list?.plus(name.asValue()) ?: listOf(name.asValue())
properties.setValue(Vision.STYLE_KEY, newStyle.asValue(), notify)
}

View File

@ -119,7 +119,7 @@ private fun CoroutineScope.collectChange(
public fun Vision.flowChanges(
collectionDuration: Duration,
): Flow<VisionChange> = flow {
val manager = manager?: error("Orphan vision could not collect changes")
val manager = manager ?: error("Orphan vision could not collect changes")
var collector = VisionChangeBuilder(manager)
coroutineScope {

View File

@ -61,23 +61,24 @@ public interface MutableVisionProperties : VisionProperties {
includeStyles,
)
public fun setProperty(
name: Name,
node: Meta?,
notify: Boolean = true,
)
public fun setValue(
name: Name,
value: Value?,
notify: Boolean = true,
)
}
public fun MutableVisionProperties.remove(name: Name){
public fun MutableVisionProperties.remove(name: Name) {
setProperty(name, null)
}
public fun MutableVisionProperties.remove(name: String){
public fun MutableVisionProperties.remove(name: String) {
remove(name.parseAsName())
}
@ -180,7 +181,7 @@ public abstract class AbstractVisionProperties(
return descriptor?.defaultValue
}
override fun setProperty(name: Name, node: Meta?) {
override fun setProperty(name: Name, node: Meta?, notify: Boolean) {
//TODO check old value?
if (name.isEmpty()) {
properties = node?.asMutableMeta()
@ -189,25 +190,42 @@ public abstract class AbstractVisionProperties(
} else {
getOrCreateProperties().setMeta(name, node)
}
invalidate(name)
if (notify) {
invalidate(name)
}
}
override fun setValue(name: Name, value: Value?) {
override fun setValue(name: Name, value: Value?, notify: Boolean) {
//TODO check old value?
if (value == null) {
properties?.getMeta(name)?.value = null
} else {
getOrCreateProperties().setValue(name, value)
}
invalidate(name)
if (notify) {
invalidate(name)
}
}
@Transient
private val _changes = MutableSharedFlow<Name>()
override val changes: SharedFlow<Name> get() = _changes
protected val changesInternal = MutableSharedFlow<Name>()
override val changes: SharedFlow<Name> get() = changesInternal
@OptIn(DelicateCoroutinesApi::class)
override fun invalidate(propertyName: Name) {
//send update signal
@OptIn(DelicateCoroutinesApi::class)
(vision.manager?.context ?: GlobalScope).launch {
changesInternal.emit(propertyName)
}
//notify children if there are any
if (vision is VisionGroup) {
vision.children.values.forEach {
it.properties.invalidate(propertyName)
}
}
// update styles
if (propertyName == Vision.STYLE_KEY) {
vision.styles.asSequence()
.mapNotNull { vision.getStyle(it) }
@ -217,9 +235,6 @@ public abstract class AbstractVisionProperties(
invalidate(it.key.asName())
}
}
(vision.manager?.context ?: GlobalScope).launch {
_changes.emit(propertyName)
}
}
}

View File

@ -86,7 +86,9 @@ public abstract class VisionTagConsumer<R>(
): T = div {
id = resolveId(name)
classes = setOf(OUTPUT_CLASS)
vision.setAsRoot(manager)
if (vision.parent == null) {
vision.setAsRoot(manager)
}
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
if (!outputMeta.isEmpty()) {
//Hard-code output configuration

View File

@ -37,7 +37,7 @@ public fun Vision.useProperty(
includeStyles: Boolean? = null,
scope: CoroutineScope? = manager?.context,
callBack: (Meta) -> Unit,
): Job = useProperty(propertyName.parseAsName(),inherit, includeStyles, scope, callBack)
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callBack)
public fun <V : Vision, T> V.useProperty(
property: KProperty1<V, T>,

View File

@ -1,9 +1,7 @@
package space.kscience.visionforge.meta
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.fetch
@ -20,6 +18,7 @@ private class TestScheme : Scheme() {
companion object : SchemeSpec<TestScheme>(::TestScheme)
}
@OptIn(ExperimentalCoroutinesApi::class)
internal class VisionPropertyTest {
private val manager = Global.fetch(VisionManager)
@ -68,56 +67,63 @@ internal class VisionPropertyTest {
val child = group.children["child"]!!
var value: Value? = null
val deferred: CompletableDeferred<Value?> = CompletableDeferred()
var callCounter = 0
child.useProperty("test", inherit = true) {
val subscription = child.useProperty("test", inherit = true) {
deferred.complete(it.value)
callCounter++
value = it.value
}
assertEquals(22, value?.int)
assertEquals(22, deferred.await()?.int)
assertEquals(1, callCounter)
child.properties.remove("test")
//Need this to avoid the race
delay(20)
assertEquals(11, child.properties.getProperty("test", inherit = true).int)
assertEquals(11, value?.int)
assertEquals(2, callCounter)
// assertEquals(11, deferred.await()?.int)
// assertEquals(2, callCounter)
subscription.cancel()
}
@Test
fun testChildrenPropertyFlow() = runTest(dispatchTimeoutMs = 200) {
val group = Global.fetch(VisionManager).group {
properties {
"test" put 11
}
group("child") {
properties {
"test" put 22
}
}
}
val child = group.children["child"]!!
launch {
child.flowPropertyValue("test", inherit = true).collectIndexed { index, value ->
if (index == 0) {
assertEquals(22, value?.int)
} else if (index == 1) {
assertEquals(11, value?.int)
cancel()
when (index) {
0 -> assertEquals(22, value?.int)
1 -> assertEquals(11, value?.int)
2 -> {
assertEquals(33, value?.int)
cancel()
}
}
}
}
//wait for subscription to be created
delay(10)
delay(5)
child.properties.remove("test")
delay(50)
group.properties["test"] = 33
}
}

View File

@ -45,7 +45,7 @@ public class FX3DPlugin : AbstractPlugin() {
}
public fun buildNode(obj: Solid): Node {
val binding = VisualObjectFXBinding(this, obj)
val binding = VisionFXBinding(this, obj)
return when (obj) {
is SolidReference -> referenceFactory(obj, binding)
is SolidGroup -> {
@ -150,7 +150,7 @@ public interface FX3DFactory<in T : Solid> {
public val type: KClass<in T>
public operator fun invoke(obj: T, binding: VisualObjectFXBinding): Node
public operator fun invoke(obj: T, binding: VisionFXBinding): Node
public companion object {
public const val TYPE: String = "fx3DFactory"

View File

@ -42,7 +42,7 @@ public class FXCompositeFactory(public val plugin: FX3DPlugin) : FX3DFactory<Com
override val type: KClass<in Composite>
get() = Composite::class
override fun invoke(obj: Composite, binding: VisualObjectFXBinding): Node {
override fun invoke(obj: Composite, binding: VisionFXBinding): Node {
val first = plugin.buildNode(obj.first) as? MeshView ?: error("Can't build node")
val second = plugin.buildNode(obj.second) as? MeshView ?: error("Can't build node")
val firstCSG = first.toCSG()

View File

@ -10,7 +10,7 @@ import kotlin.reflect.KClass
public object FXConvexFactory : FX3DFactory<Convex> {
override val type: KClass<in Convex> get() = Convex::class
override fun invoke(obj: Convex, binding: VisualObjectFXBinding): Node {
override fun invoke(obj: Convex, binding: VisionFXBinding): Node {
val hull = HullUtil.hull(
obj.points.map { Vector3d.xyz(it.x.toDouble(), it.y.toDouble(), it.z.toDouble()) },
PropertyStorage()

View File

@ -14,15 +14,17 @@ import kotlin.reflect.KClass
public class FXReferenceFactory(public val plugin: FX3DPlugin) : FX3DFactory<SolidReference> {
override val type: KClass<in SolidReference> get() = SolidReference::class
override fun invoke(obj: SolidReference, binding: VisualObjectFXBinding): Node {
override fun invoke(obj: SolidReference, binding: VisionFXBinding): Node {
val prototype = obj.prototype
val node = plugin.buildNode(prototype)
obj.onPropertyChange { name->
obj.onPropertyChange { name ->
if (name.firstOrNull()?.body == REFERENCE_CHILD_PROPERTY_PREFIX) {
val childName = name.firstOrNull()?.index?.let(Name::parse) ?: error("Wrong syntax for reference child property: '$name'")
val childName = name.firstOrNull()?.index?.let(Name::parse)
?: error("Wrong syntax for reference child property: '$name'")
val propertyName = name.cutFirst()
val referenceChild = obj.children.getChild(childName) ?: error("Reference child with name '$childName' not found")
val referenceChild =
obj.children.getChild(childName) ?: error("Reference child with name '$childName' not found")
val child = node.findChild(childName) ?: error("Object child with name '$childName' not found")
child.updateProperty(referenceChild, propertyName)
}

View File

@ -11,7 +11,7 @@ import kotlin.reflect.KClass
public object FXShapeFactory : FX3DFactory<GeometrySolid> {
override val type: KClass<in GeometrySolid> get() = GeometrySolid::class
override fun invoke(obj: GeometrySolid, binding: VisualObjectFXBinding): MeshView {
override fun invoke(obj: GeometrySolid, binding: VisionFXBinding): MeshView {
val mesh = FXGeometryBuilder().apply { obj.toGeometry(this) }.build()
return MeshView(mesh)
}

View File

@ -12,7 +12,7 @@ import tornadofx.*
/**
* A caching binding collection for [Vision] properties
*/
public class VisualObjectFXBinding(public val fx: FX3DPlugin, public val obj: Vision) {
public class VisionFXBinding(public val fx: FX3DPlugin, public val obj: Vision) {
private val bindings = HashMap<Name, ObjectBinding<Meta?>>()
init {

View File

@ -30,7 +30,7 @@ public class GdmlLoaderOptions {
styleCache.getOrPut(Name.parse(name)) {
Meta(builder)
}
useStyle(name)
useStyle(name, false)
}
public fun Solid.transparent() {

View File

@ -352,7 +352,7 @@ private class GdmlLoader(val settings: GdmlLoaderOptions) {
val rootStyle by final.style("gdml") {
Solid.ROTATION_ORDER_KEY put RotationOrder.ZXY
}
final.useStyle(rootStyle)
final.useStyle(rootStyle, false)
final.prototypes {
proto.items.forEach { (token, item) ->

View File

@ -1,6 +1,9 @@
package space.kscience.visionforge.solid
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@ -65,6 +68,25 @@ public class SolidReference(
override fun getValue(name: Name, inherit: Boolean?, includeStyles: Boolean?): Value? {
return properties?.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles)
}
override fun invalidate(propertyName: Name) {
//send update signal
@OptIn(DelicateCoroutinesApi::class)
(manager?.context ?: GlobalScope).launch {
changesInternal.emit(propertyName)
}
// update styles
if (propertyName == Vision.STYLE_KEY) {
styles.asSequence()
.mapNotNull { getStyle(it) }
.flatMap { it.items.asSequence() }
.distinctBy { it.key }
.forEach {
invalidate(it.key.asName())
}
}
}
}
}
@ -117,11 +139,11 @@ internal class SolidReferenceChild(
includeStyles: Boolean?,
): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles)
override fun setProperty(name: Name, node: Meta?) {
override fun setProperty(name: Name, node: Meta?, notify: Boolean) {
own.setMeta(name, node)
}
override fun setValue(name: Name, value: Value?) {
override fun setValue(name: Name, value: Value?, notify: Boolean) {
own.setValue(name, value)
}

View File

@ -2,6 +2,7 @@ package space.kscience.visionforge.solid
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
@ -10,8 +11,9 @@ import space.kscience.visionforge.*
import kotlin.test.Test
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("UNUSED_VARIABLE")
class PropertyTest {
class SolidPropertyTest {
@Test
fun testColor() {
val box = Box(10.0f, 10.0f, 10.0f)
@ -23,7 +25,6 @@ class PropertyTest {
assertEquals("pink", box.color.string)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun testColorUpdate() = runTest(dispatchTimeoutMs = 200) {
val box = Box(10.0f, 10.0f, 10.0f)
@ -31,11 +32,12 @@ class PropertyTest {
val c = CompletableDeferred<String?>()
val subscription = box.onPropertyChange(this) {
if (it == SolidMaterial.MATERIAL_COLOR_KEY) {
val subscription = box.onPropertyChange(this) { key ->
if (key == SolidMaterial.MATERIAL_COLOR_KEY) {
c.complete(box.color.string)
}
}
delay(5)
box.material {
color.set("pink")

View File

@ -27,7 +27,7 @@ public object ThreeLabelFactory : ThreeFactory<SolidLabel> {
return Mesh(textGeo, ThreeMaterials.DEFAULT).apply {
updateMaterial(obj)
updatePosition(obj)
obj.onPropertyChange { _ ->
obj.onPropertyChange {
//TODO
three.logger.warn { "Label parameter change not implemented" }
}

View File

@ -43,7 +43,7 @@ public abstract class ThreeMeshFactory<in T : Solid>(
}
//add listener to object properties
obj.onPropertyChange { name ->
obj.onPropertyChange { name->
when {
name.startsWith(Solid.GEOMETRY_KEY) -> {
val oldGeometry = mesh.geometry

View File

@ -11,6 +11,7 @@ import space.kscience.dataforge.meta.update
import space.kscience.dataforge.names.*
import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.Vision
import space.kscience.visionforge.onPropertyChange
import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
import space.kscience.visionforge.visible
@ -68,7 +69,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
updatePosition(obj)
//obj.onChildrenChange()
obj.properties.changes.onEach { name ->
obj.onPropertyChange(context) { name ->
if (
name.startsWith(Solid.POSITION_KEY) ||
name.startsWith(Solid.ROTATION_KEY) ||
@ -79,7 +80,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
} else if (name == Vision.VISIBLE_KEY) {
visible = obj.visible ?: true
}
}.launchIn(context)
}
obj.children.changes.onEach { childName ->
val child = obj.children.getChild(childName)
@ -101,6 +102,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
}.launchIn(context)
}
}
is Composite -> compositeFactory.build(this, obj)
else -> {
//find specialized factory for this type if it is present
@ -179,6 +181,7 @@ internal fun Object3D.getOrCreateGroup(name: Name): Object3D {
this.add(group)
}
}
else -> getOrCreateGroup(name.tokens.first().asName()).getOrCreateGroup(name.cutFirst())
}
}