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 } vision("form") { form }
form.onPropertyChange { form.onPropertyChange { _, _ ->
println(this) 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) material.color.setRGB(r.toFloat() / 256, g.toFloat() / 256, b.toFloat() / 256)
mesh.updateMatrix() mesh.updateMatrix()
} }
name.startsWith(ThreeMeshFactory.EDGES_KEY) -> mesh.applyEdges(this@VariableBox) name.startsWith(ThreeMeshFactory.EDGES_KEY) -> mesh.applyEdges(this@VariableBox)
else -> mesh.updateProperty(this@VariableBox, name) 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) } val descriptor: MetaDescriptor? = useMemo(props.descriptor, props.name) { props.descriptor?.get(props.name) }
var property: MutableMeta by useState { props.meta.getOrCreate(props.name) } var property: MutableMeta by useState { props.meta.getOrCreate(props.name) }
val defined = props.getPropertyState(props.name) == EditorPropertyState.Defined
val keys = useMemo(descriptor) { val keys = useMemo(descriptor) {
buildSet { buildSet {
descriptor?.children?.filterNot { descriptor?.children?.filterNot {
@ -134,7 +136,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
styledSpan { styledSpan {
css { css {
+TreeStyles.treeLabel +TreeStyles.treeLabel
if (property.isEmpty()) { if (!defined) {
+TreeStyles.treeLabelInactive +TreeStyles.treeLabelInactive
} }
} }
@ -175,7 +177,7 @@ private fun RBuilder.propertyEditorItem(props: PropertyEditorProps) {
} }
+"\u00D7" +"\u00D7"
attrs { attrs {
if (property.isEmpty()) { if (!defined) {
disabled = true disabled = true
} else { } else {
onClickFunction = removeClick onClickFunction = removeClick

View File

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

View File

@ -18,10 +18,10 @@ private tailrec fun styleIsDefined(vision: Vision, reference: StyleReference): B
} }
@VisionBuilder @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 that style is defined in a parent
//check(styleIsDefined(this, reference)) { "Style reference does not belong to a Vision parent" } //check(styleIsDefined(this, reference)) { "Style reference does not belong to a Vision parent" }
useStyle(reference.name) useStyle(reference.name, notify)
} }
@VisionBuilder @VisionBuilder

View File

@ -83,10 +83,12 @@ public var Vision.styles: List<String>
public val Vision.styleSheet: StyleSheet get() = StyleSheet(this) 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) { public fun Vision.useStyle(name: String, notify: Boolean = true) {
styles = (properties.own?.get(Vision.STYLE_KEY)?.stringList ?: emptyList()) + name 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

@ -61,15 +61,16 @@ public interface MutableVisionProperties : VisionProperties {
includeStyles, includeStyles,
) )
public fun setProperty( public fun setProperty(
name: Name, name: Name,
node: Meta?, node: Meta?,
notify: Boolean = true,
) )
public fun setValue( public fun setValue(
name: Name, name: Name,
value: Value?, value: Value?,
notify: Boolean = true,
) )
} }
@ -180,7 +181,7 @@ public abstract class AbstractVisionProperties(
return descriptor?.defaultValue return descriptor?.defaultValue
} }
override fun setProperty(name: Name, node: Meta?) { override fun setProperty(name: Name, node: Meta?, notify: Boolean) {
//TODO check old value? //TODO check old value?
if (name.isEmpty()) { if (name.isEmpty()) {
properties = node?.asMutableMeta() properties = node?.asMutableMeta()
@ -189,25 +190,42 @@ public abstract class AbstractVisionProperties(
} else { } else {
getOrCreateProperties().setMeta(name, node) getOrCreateProperties().setMeta(name, node)
} }
if (notify) {
invalidate(name) invalidate(name)
} }
}
override fun setValue(name: Name, value: Value?) { override fun setValue(name: Name, value: Value?, notify: Boolean) {
//TODO check old value? //TODO check old value?
if (value == null) { if (value == null) {
properties?.getMeta(name)?.value = null properties?.getMeta(name)?.value = null
} else { } else {
getOrCreateProperties().setValue(name, value) getOrCreateProperties().setValue(name, value)
} }
if (notify) {
invalidate(name) invalidate(name)
} }
}
@Transient @Transient
private val _changes = MutableSharedFlow<Name>() protected val changesInternal = MutableSharedFlow<Name>()
override val changes: SharedFlow<Name> get() = _changes override val changes: SharedFlow<Name> get() = changesInternal
@OptIn(DelicateCoroutinesApi::class)
override fun invalidate(propertyName: Name) { 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) { if (propertyName == Vision.STYLE_KEY) {
vision.styles.asSequence() vision.styles.asSequence()
.mapNotNull { vision.getStyle(it) } .mapNotNull { vision.getStyle(it) }
@ -217,9 +235,6 @@ public abstract class AbstractVisionProperties(
invalidate(it.key.asName()) 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 { ): T = div {
id = resolveId(name) id = resolveId(name)
classes = setOf(OUTPUT_CLASS) classes = setOf(OUTPUT_CLASS)
if (vision.parent == null) {
vision.setAsRoot(manager) vision.setAsRoot(manager)
}
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString() attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
if (!outputMeta.isEmpty()) { if (!outputMeta.isEmpty()) {
//Hard-code output configuration //Hard-code output configuration

View File

@ -1,9 +1,7 @@
package space.kscience.visionforge.meta package space.kscience.visionforge.meta
import kotlinx.coroutines.cancel import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.fetch import space.kscience.dataforge.context.fetch
@ -20,6 +18,7 @@ private class TestScheme : Scheme() {
companion object : SchemeSpec<TestScheme>(::TestScheme) companion object : SchemeSpec<TestScheme>(::TestScheme)
} }
@OptIn(ExperimentalCoroutinesApi::class)
internal class VisionPropertyTest { internal class VisionPropertyTest {
private val manager = Global.fetch(VisionManager) private val manager = Global.fetch(VisionManager)
@ -68,56 +67,63 @@ internal class VisionPropertyTest {
val child = group.children["child"]!! val child = group.children["child"]!!
var value: Value? = null val deferred: CompletableDeferred<Value?> = CompletableDeferred()
var callCounter = 0 var callCounter = 0
child.useProperty("test", inherit = true) { val subscription = child.useProperty("test", inherit = true) {
deferred.complete(it.value)
callCounter++ callCounter++
value = it.value
} }
assertEquals(22, value?.int) assertEquals(22, deferred.await()?.int)
assertEquals(1, callCounter) assertEquals(1, callCounter)
child.properties.remove("test") child.properties.remove("test")
//Need this to avoid the race
delay(20)
assertEquals(11, child.properties.getProperty("test", inherit = true).int) assertEquals(11, child.properties.getProperty("test", inherit = true).int)
assertEquals(11, value?.int) // assertEquals(11, deferred.await()?.int)
assertEquals(2, callCounter) // assertEquals(2, callCounter)
subscription.cancel()
} }
@Test @Test
fun testChildrenPropertyFlow() = runTest(dispatchTimeoutMs = 200) { fun testChildrenPropertyFlow() = runTest(dispatchTimeoutMs = 200) {
val group = Global.fetch(VisionManager).group { val group = Global.fetch(VisionManager).group {
properties { properties {
"test" put 11 "test" put 11
} }
group("child") { group("child") {
properties { properties {
"test" put 22 "test" put 22
} }
} }
} }
val child = group.children["child"]!! val child = group.children["child"]!!
launch { launch {
child.flowPropertyValue("test", inherit = true).collectIndexed { index, value -> child.flowPropertyValue("test", inherit = true).collectIndexed { index, value ->
if (index == 0) { when (index) {
assertEquals(22, value?.int) 0 -> assertEquals(22, value?.int)
} else if (index == 1) { 1 -> assertEquals(11, value?.int)
assertEquals(11, value?.int) 2 -> {
assertEquals(33, value?.int)
cancel() cancel()
} }
} }
} }
}
//wait for subscription to be created //wait for subscription to be created
delay(10) delay(5)
child.properties.remove("test") 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 { public fun buildNode(obj: Solid): Node {
val binding = VisualObjectFXBinding(this, obj) val binding = VisionFXBinding(this, obj)
return when (obj) { return when (obj) {
is SolidReference -> referenceFactory(obj, binding) is SolidReference -> referenceFactory(obj, binding)
is SolidGroup -> { is SolidGroup -> {
@ -150,7 +150,7 @@ public interface FX3DFactory<in T : Solid> {
public val type: KClass<in T> 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 companion object {
public const val TYPE: String = "fx3DFactory" 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> override val type: KClass<in Composite>
get() = Composite::class 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 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 second = plugin.buildNode(obj.second) as? MeshView ?: error("Can't build node")
val firstCSG = first.toCSG() val firstCSG = first.toCSG()

View File

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

View File

@ -14,15 +14,17 @@ import kotlin.reflect.KClass
public class FXReferenceFactory(public val plugin: FX3DPlugin) : FX3DFactory<SolidReference> { public class FXReferenceFactory(public val plugin: FX3DPlugin) : FX3DFactory<SolidReference> {
override val type: KClass<in SolidReference> get() = SolidReference::class 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 prototype = obj.prototype
val node = plugin.buildNode(prototype) val node = plugin.buildNode(prototype)
obj.onPropertyChange { name -> obj.onPropertyChange { name ->
if (name.firstOrNull()?.body == REFERENCE_CHILD_PROPERTY_PREFIX) { 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 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") val child = node.findChild(childName) ?: error("Object child with name '$childName' not found")
child.updateProperty(referenceChild, propertyName) child.updateProperty(referenceChild, propertyName)
} }

View File

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

View File

@ -12,7 +12,7 @@ import tornadofx.*
/** /**
* A caching binding collection for [Vision] properties * 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?>>() private val bindings = HashMap<Name, ObjectBinding<Meta?>>()
init { init {

View File

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

View File

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

View File

@ -1,6 +1,9 @@
package space.kscience.visionforge.solid package space.kscience.visionforge.solid
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
@ -65,6 +68,25 @@ public class SolidReference(
override fun getValue(name: Name, inherit: Boolean?, includeStyles: Boolean?): Value? { override fun getValue(name: Name, inherit: Boolean?, includeStyles: Boolean?): Value? {
return properties?.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles) 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?, includeStyles: Boolean?,
): Value? = own.getValue(name) ?: prototype.properties.getValue(name, inherit, includeStyles) ): 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) own.setMeta(name, node)
} }
override fun setValue(name: Name, value: Value?) { override fun setValue(name: Name, value: Value?, notify: Boolean) {
own.setValue(name, value) own.setValue(name, value)
} }

View File

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

View File

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

View File

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