diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegates.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegates.kt index 7a35f3e8..e3c42de4 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegates.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegates.kt @@ -39,6 +39,9 @@ class DisplayObjectDelegateWrapper( val write: Config.(name: Name, value: T) -> Unit = { name, value -> set(name, value) }, val read: (MetaItem<*>?) -> T? ) : ReadWriteProperty { + + //private var cachedName: Name? = null + override fun getValue(thisRef: VisualObject, property: KProperty<*>): T { val name = key ?: property.name.toName() return if (inherited) { diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep.dataforge.vis.hmr/HMR.kt b/dataforge-vis-common/src/jsMain/kotlin/hep.dataforge.vis.hmr/HMR.kt new file mode 100644 index 00000000..37b4c4c1 --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep.dataforge.vis.hmr/HMR.kt @@ -0,0 +1,64 @@ +package hep.dataforge.vis.hmr + +import kotlin.browser.document +import kotlin.dom.hasClass + +external val module: Module + +external interface Module { + val hot: Hot? +} + +external interface Hot { + val data: dynamic + + fun accept() + fun accept(dependency: String, callback: () -> Unit) + fun accept(dependencies: Array, callback: (updated: Array) -> Unit) + + fun dispose(callback: (data: dynamic) -> Unit) +} + +external fun require(name: String): dynamic + +abstract class ApplicationBase { + open val stateKeys: List get() = emptyList() + + abstract fun start(state: Map) + open fun dispose(): Map = emptyMap() +} + +fun startApplication(builder: () -> ApplicationBase) { + fun start(state: dynamic): ApplicationBase? { + return if (document.body?.hasClass("testApp") == true) { + val application = builder() + + @Suppress("UnsafeCastFromDynamic") + application.start(state?.appState ?: emptyMap()) + + application + } else { + null + } + } + + var application: ApplicationBase? = null + + val state: dynamic = module.hot?.let { hot -> + hot.accept() + + hot.dispose { data -> + data.appState = application?.dispose() + application = null + } + + hot.data + } + + if (document.body != null) { + application = start(state) + } else { + application = null + document.addEventListener("DOMContentLoaded", { application = start(state) }) + } +} \ No newline at end of file diff --git a/dataforge-vis-jsroot/src/main/web/index.html b/dataforge-vis-jsroot/src/main/web/index.html index 1f49cf4e..1fa719ab 100644 --- a/dataforge-vis-jsroot/src/main/web/index.html +++ b/dataforge-vis-jsroot/src/main/web/index.html @@ -6,7 +6,7 @@ Three js demo for particle physics - +
Meta ): VisualGroup { - val solid = - gdmlVolume.solidref.resolve(root) - ?: error("Solid with tag ${gdmlVolume.solidref.ref} for volume ${gdmlVolume.name} not defined") - val material = - gdmlVolume.materialref.resolve(root) - ?: error("Material with tag ${gdmlVolume.materialref.ref} for volume ${gdmlVolume.name} not defined") + if (group is GDMLVolume) { + val solid = group.solidref.resolve(root) + ?: error("Solid with tag ${group.solidref.ref} for volume ${group.name} not defined") + val material = group.materialref.resolve(root) + ?: error("Material with tag ${group.materialref.ref} for volume ${group.name} not defined") - addSolid(root, solid) { - color(material.resolveColor()) + addSolid(root, solid) { + color(material.resolveColor()) + } + //TODO render placements } - gdmlVolume.physVolumes.forEach { - val volume = it.volumeref.resolve(root) ?: error("Volume with ref ${it.volumeref.ref} could not be resolved") + group.physVolumes.forEach { + val volume: GDMLGroup = + it.volumeref.resolve(root) ?: error("Volume with ref ${it.volumeref.ref} could not be resolved") addVolume(root, volume, resolveColor).apply { it.resolvePosition(root)?.let { pos -> applyPosition(pos) } it.resolveRotation(root)?.let { rot -> applyRotation(rot) } diff --git a/dataforge-vis-spatial-gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt b/dataforge-vis-spatial-gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt new file mode 100644 index 00000000..328a28fb --- /dev/null +++ b/dataforge-vis-spatial-gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt @@ -0,0 +1,82 @@ +package hep.dataforge.vis.spatial.gdml.demo + +import hep.dataforge.context.Global +import hep.dataforge.vis.hmr.ApplicationBase +import hep.dataforge.vis.hmr.startApplication +import hep.dataforge.vis.spatial.gdml.toVisual +import hep.dataforge.vis.spatial.three.ThreePlugin +import hep.dataforge.vis.spatial.three.output +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.events.Event +import org.w3c.files.FileList +import org.w3c.files.FileReader +import org.w3c.files.get +import scientifik.gdml.GDML +import kotlin.browser.document +import kotlin.dom.clear + +class GDMLDemoApp : ApplicationBase() { + + + /** + * Handle mouse drag according to https://www.html5rocks.com/en/tutorials/file/dndfiles/ + */ + private fun handleDragOver(event: Event) { + event.stopPropagation() + event.preventDefault() + event.asDynamic().dataTransfer.dropEffect = "copy" + } + + /** + * Load data from text file + */ + private fun loadData(event: Event, block: suspend (String) -> Unit) { + event.stopPropagation() + event.preventDefault() + + + val file = (event.asDynamic().dataTransfer.files as FileList)[0] + ?: throw RuntimeException("Failed to load file") + + FileReader().apply { + onload = { + val string = result as String + GlobalScope.launch { + block(string) + } + } + readAsText(file) + } + } + + + override fun start(state: Map) { + + val context = Global.context("demo") {} + val three = context.plugins.load(ThreePlugin) + val canvas = document.getElementById("canvas") ?: error("Element with id canvas not found on page") + canvas.clear() + val output = three.output(canvas) + //val url = URL("https://drive.google.com/open?id=1w5e7fILMN83JGgB8WANJUYm8OW2s0WVO") + + + val action: suspend (String) -> Unit = { + val gdml = GDML.format.parse(GDML.serializer(), it) + val visual = gdml.toVisual() + output.render(visual) + } + + (document.getElementById("drop_zone") as? HTMLDivElement)?.apply { + addEventListener("dragover", { handleDragOver(it) }, false) + addEventListener("drop", { loadData(it, action) }, false) + } + + } + +} + +fun main() { + startApplication(::GDMLDemoApp) +} \ No newline at end of file diff --git a/dataforge-vis-spatial-gdml/src/jsMain/web/index.html b/dataforge-vis-spatial-gdml/src/jsMain/web/index.html new file mode 100644 index 00000000..bb3faf0a --- /dev/null +++ b/dataforge-vis-spatial-gdml/src/jsMain/web/index.html @@ -0,0 +1,33 @@ + + + + + + Three js demo for particle physics + + + + +
+ Загрузить данные +
+ (перетащить файл сюда) +
+
+

GDML demo

+
+
+ + + + + + \ No newline at end of file diff --git a/dataforge-vis-spatial-gdml/src/jvmTest/kotlin/hep/dataforge/vis/spatial/gdml/BMNTest.kt b/dataforge-vis-spatial-gdml/src/jvmTest/kotlin/hep/dataforge/vis/spatial/gdml/BMNTest.kt new file mode 100644 index 00000000..a04efbe5 --- /dev/null +++ b/dataforge-vis-spatial-gdml/src/jvmTest/kotlin/hep/dataforge/vis/spatial/gdml/BMNTest.kt @@ -0,0 +1,28 @@ +package hep.dataforge.vis.spatial.gdml + +import nl.adaptivity.xmlutil.StAXReader +import org.junit.Test +import scientifik.gdml.GDML +import java.io.File +import java.net.URL + +class BMNTest { + + @Test + fun testRead() { + + val url = URL("https://drive.google.com/open?id=1w5e7fILMN83JGgB8WANJUYm8OW2s0WVO") + val file = File("D:\\Work\\Projects\\gdml.kt\\src\\commonTest\\resources\\gdml\\geofile_full.xml") + val stream = if(file.exists()){ + file.inputStream() + } else { + url.openStream() + } + + val xmlReader = StAXReader(stream, "UTF-8") + val xml = GDML.format.parse(GDML.serializer(), xmlReader) + repeat(5) { + xml.toVisual() + } + } +} \ No newline at end of file diff --git a/dataforge-vis-spatial-js/build.gradle.kts b/dataforge-vis-spatial-js/build.gradle.kts index 588cd849..5414ef13 100644 --- a/dataforge-vis-spatial-js/build.gradle.kts +++ b/dataforge-vis-spatial-js/build.gradle.kts @@ -1,69 +1,21 @@ plugins { id("scientifik.js") - id("kotlin-dce-js") + //id("kotlin-dce-js") } //val kotlinVersion: String by rootProject.extra dependencies { api(project(":dataforge-vis-spatial")) - api(project(":dataforge-vis-spatial-gdml")) - api("info.laht.threekt:threejs-wrapper:0.106-npm-2") + api("info.laht.threekt:threejs-wrapper:0.106-npm-3") testCompile(kotlin("test-js")) } kotlin{ sourceSets["main"].dependencies{ - api(npm("three","0.106.2")) + implementation(npm("three","0.106.2")) implementation(npm("@hi-level/three-csg")) implementation(npm("style-loader")) implementation(npm("element-resize-event")) } } - -// -//configure { -// downloadNodeJsVersion = "latest" -// -// configure { -// dependency("three","0.106.2") -// dependency("@hi-level/three-csg") -// dependency("style-loader") -// dependency("element-resize-event") -// devDependency("karma") -// } -// -// sourceMaps = true -// -// bundle("webpack") { -// this as WebPackExtension -// bundleName = "main" -// contentPath = file("src/main/web") -// sourceMapEnabled = true -// //mode = "production" -// mode = "development" -// } -//} -// -//tasks { -// "compileKotlin2Js"(Kotlin2JsCompile::class) { -// kotlinOptions { -// metaInfo = true -// outputFile = "${project.buildDir.path}/js/${project.name}.js" -// sourceMap = true -// moduleKind = "commonjs" -// main = "call" -// kotlinOptions.sourceMapEmbedSources = "always" -// } -// } -// -// "compileTestKotlin2Js"(Kotlin2JsCompile::class) { -// kotlinOptions { -// metaInfo = true -// outputFile = "${project.buildDir.path}/js/${project.name}-test.js" -// sourceMap = true -// moduleKind = "commonjs" -// kotlinOptions.sourceMapEmbedSources = "always" -// } -// } -//} \ No newline at end of file diff --git a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/HMR.kt b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/HMR.kt deleted file mode 100644 index d5f5fffe..00000000 --- a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/HMR.kt +++ /dev/null @@ -1,19 +0,0 @@ -package hep.dataforge.vis.spatial.demo - -external val module: Module - -external interface Module { - val hot: Hot? -} - -external interface Hot { - val data: dynamic - - fun accept() - fun accept(dependency: String, callback: () -> Unit) - fun accept(dependencies: Array, callback: (updated: Array) -> Unit) - - fun dispose(callback: (data: dynamic) -> Unit) -} - -external fun require(name: String): dynamic \ No newline at end of file diff --git a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/ThreeDemoApp.kt b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/ThreeDemoApp.kt index 77759c74..3234ade9 100644 --- a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/ThreeDemoApp.kt +++ b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/ThreeDemoApp.kt @@ -4,6 +4,8 @@ import hep.dataforge.context.ContextBuilder import hep.dataforge.meta.number import hep.dataforge.vis.common.Colors import hep.dataforge.vis.common.color +import hep.dataforge.vis.hmr.ApplicationBase +import hep.dataforge.vis.hmr.startApplication import hep.dataforge.vis.spatial.* import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -135,4 +137,8 @@ class ThreeDemoApp : ApplicationBase() { } override fun dispose() = emptyMap()//mapOf("lines" to presenter.dispose()) +} + +fun main() { + startApplication(::ThreeDemoApp) } \ No newline at end of file diff --git a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/ThreeDemoGrid.kt b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/ThreeDemoGrid.kt index 558192fd..a9746081 100644 --- a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/ThreeDemoGrid.kt +++ b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/ThreeDemoGrid.kt @@ -50,7 +50,7 @@ class ThreeDemoGrid(meta: Meta) : AbstractPlugin(meta), OutputManager { return outputs.getOrPut(name) { if (type != VisualObject::class) error("Supports only DisplayObject") - val output = three.output(meta) { + val output = three.output(meta = meta) { "axis" to { "size" to 500 } diff --git a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/main.kt b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/main.kt deleted file mode 100644 index 9672c9ef..00000000 --- a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/demo/main.kt +++ /dev/null @@ -1,48 +0,0 @@ -package hep.dataforge.vis.spatial.demo - -import kotlin.browser.document -import kotlin.dom.hasClass - - -abstract class ApplicationBase { - abstract val stateKeys: List - - abstract fun start(state: Map) - abstract fun dispose(): Map -} - - -fun main() { - var application: ApplicationBase? = null - - val state: dynamic = module.hot?.let { hot -> - hot.accept() - - hot.dispose { data -> - data.appState = application?.dispose() - application = null - } - - hot.data - } - - if (document.body != null) { - application = start(state) - } else { - application = null - document.addEventListener("DOMContentLoaded", { application = start(state) }) - } -} - -fun start(state: dynamic): ApplicationBase? { - return if (document.body?.hasClass("testApp") == true) { - val application = ThreeDemoApp() - - @Suppress("UnsafeCastFromDynamic") - application.start(state?.appState ?: emptyMap()) - - application - } else { - null - } -} \ No newline at end of file diff --git a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreeCylinderFactory.kt b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreeCylinderFactory.kt new file mode 100644 index 00000000..756d2116 --- /dev/null +++ b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreeCylinderFactory.kt @@ -0,0 +1,32 @@ +package hep.dataforge.vis.spatial.three + +import hep.dataforge.vis.spatial.Cylinder +import hep.dataforge.vis.spatial.detail +import info.laht.threekt.core.BufferGeometry +import info.laht.threekt.geometries.CylinderBufferGeometry +import kotlin.math.pow + +object ThreeCylinderFactory : MeshThreeFactory(Cylinder::class) { + override fun buildGeometry(obj: Cylinder): BufferGeometry { + return obj.detail?.let { + val segments = it.toDouble().pow(0.5).toInt() + CylinderBufferGeometry( + radiusTop = obj.upperRadius!!, + radiusBottom = obj.radius!!, + height = obj.height!!, + radialSegments = segments, + heightSegments = segments, + openEnded = false, + thetaStart = obj.startAngle, + thetaLength = obj.angle + ) + } ?: CylinderBufferGeometry( + radiusTop = obj.upperRadius!!, + radiusBottom = obj.radius!!, + height = obj.height!!, + openEnded = false, + thetaStart = obj.startAngle, + thetaLength = obj.angle + ) + } +} \ No newline at end of file diff --git a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreeOutput.kt b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreeOutput.kt index 5536b6de..d7c1e78d 100644 --- a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreeOutput.kt +++ b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreeOutput.kt @@ -5,7 +5,7 @@ import hep.dataforge.meta.* import hep.dataforge.output.Output import hep.dataforge.vis.common.Colors import hep.dataforge.vis.common.VisualObject -import hep.dataforge.vis.spatial.demo.require +import hep.dataforge.vis.hmr.require import info.laht.threekt.WebGLRenderer import info.laht.threekt.helpers.AxesHelper import info.laht.threekt.lights.AmbientLight @@ -39,7 +39,7 @@ class ThreeOutput(val three: ThreePlugin, val meta: Meta = EmptyMeta) : Output Unit = {}) = - ThreeOutput(this, buildMeta(meta, override)) \ No newline at end of file +fun ThreePlugin.output(element: Element? = null, meta: Meta = EmptyMeta, override: MetaBuilder.() -> Unit = {}) = + ThreeOutput(this, buildMeta(meta, override)).apply { + if(element!=null){ + attach(element) + } + } \ No newline at end of file diff --git a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreePlugin.kt b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreePlugin.kt index 0914dd4f..5c46b5d9 100644 --- a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreePlugin.kt +++ b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/three/ThreePlugin.kt @@ -28,6 +28,7 @@ class ThreePlugin : AbstractPlugin() { objectFactories[Box::class] = ThreeBoxFactory objectFactories[Convex::class] = ThreeConvexFactory objectFactories[Sphere::class] = ThreeSphereFactory + objectFactories[Cylinder::class] = ThreeCylinderFactory } private fun findObjectFactory(type: KClass): ThreeFactory<*>? { diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Composite.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Composite.kt index 33abade8..ac987e1d 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Composite.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Composite.kt @@ -25,7 +25,9 @@ fun VisualGroup.composite(type: CompositeType, builder: VisualGroup.() -> Unit): val group = VisualGroup().apply(builder) val children = group.toList() if (children.size != 2) error("Composite requires exactly two children") - return Composite(this, children[0], children[1], type, group.properties.seal()).also { add(it) } + return Composite(this, children[0], children[1], type, group.properties.seal()).also { + this.add(it) + } } fun VisualGroup.union(builder: VisualGroup.() -> Unit) = diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Cylinder.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Cylinder.kt index 93a1bede..8e04be1d 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Cylinder.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Cylinder.kt @@ -16,13 +16,13 @@ class Cylinder(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta) { var upperRadius by number(default = radius) var height by number() var startAngle by number(0.0) - var angle by number(2* PI) + var angle by number(2 * PI) } -fun VisualGroup.cylinder(r: Number, height: Number, meta: Meta = EmptyMeta, block: Cylinder.()->Unit = {}):Cylinder{ - val cylinder = Cylinder(this,meta) +fun VisualGroup.cylinder(r: Number, height: Number, meta: Meta = EmptyMeta, block: Cylinder.() -> Unit = {}): Cylinder { + val cylinder = Cylinder(this, meta) cylinder.radius = r cylinder.height = height cylinder.apply(block) - return cylinder + return cylinder.also { add(it) } } \ No newline at end of file diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/displayObject3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/displayObject3D.kt index 7cbc28d4..3d1ebfdd 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/displayObject3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/displayObject3D.kt @@ -1,6 +1,7 @@ package hep.dataforge.vis.spatial import hep.dataforge.meta.* +import hep.dataforge.names.toName import hep.dataforge.output.Output import hep.dataforge.vis.common.VisualGroup import hep.dataforge.vis.common.VisualObject @@ -28,60 +29,71 @@ var VisualObject.visible // 3D Object position +private val xPos = "pos.x".toName() /** * x position property relative to parent. Not inherited */ var VisualObject.x - get() = properties["pos.x"].number ?: 0.0 + get() = properties[xPos].number ?: 0.0 set(value) { - properties["pos.x"] = value + properties[xPos] = value } +private val yPos = "pos.y".toName() + /** * y position property. Not inherited */ var VisualObject.y - get() = properties["pos.y"].number ?: 0.0 + get() = properties[yPos].number ?: 0.0 set(value) { - properties["pos.y"] = value + properties[yPos] = value } +private val zPos = "pos.z".toName() + /** * z position property. Not inherited */ var VisualObject.z - get() = properties["pos.z"].number ?: 0.0 + get() = properties[zPos].number ?: 0.0 set(value) { - properties["pos.z"] = value + properties[zPos] = value } // 3D Object rotation +private val xRotation = "rotation.x".toName() + /** * x rotation relative to parent. Not inherited */ var VisualObject.rotationX - get() = properties["rotation.x"].number ?: 0.0 + get() = properties[xRotation].number ?: 0.0 set(value) { - properties["rotation.x"] = value + properties[xRotation] = value } +private val yRotation = "rotation.y".toName() + /** * y rotation relative to parent. Not inherited */ var VisualObject.rotationY - get() = properties["rotation.y"].number ?: 0.0 + get() = properties[yRotation].number ?: 0.0 set(value) { - properties["rotation.y"] = value + properties[yRotation] = value } +private val zRotation = "rotation.z".toName() + /** * z rotation relative to parent. Not inherited */ var VisualObject.rotationZ - get() = properties["rotation.z"].number ?: 0.0 + get() = properties[zRotation].number ?: 0.0 set(value) { - properties["rotation.z"] = value + properties[zRotation] = value } enum class RotationOrder {