GDML demo renders something

This commit is contained in:
Alexander Nozik 2019-07-27 20:05:44 +03:00
parent 687393c243
commit d3500c3a57
23 changed files with 553 additions and 438 deletions

View File

@ -4,6 +4,7 @@ import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaBuilder
import hep.dataforge.meta.buildMeta
import hep.dataforge.meta.set
import hep.dataforge.names.toName
/**
* Taken from https://github.com/markaren/three.kt/blob/master/threejs-wrapper/src/main/kotlin/info/laht/threekt/math/ColorConstants.kt
@ -181,13 +182,14 @@ object Colors {
const val yellowgreen = 0x9ACD32
}
private val material = "material".toName()
fun VisualObject.color(rgb: Int) {
this.properties["material"] = rgb
this.config[material] = rgb
}
fun VisualObject.color(meta: Meta) {
this.properties["material"] = meta
this.config[material] = meta
}
fun VisualObject.color(builder: MetaBuilder.() -> Unit) {

View File

@ -1,29 +1,41 @@
package hep.dataforge.vis.common
import hep.dataforge.meta.EmptyMeta
import hep.dataforge.meta.Config
import hep.dataforge.meta.Laminate
import hep.dataforge.meta.Meta
import hep.dataforge.meta.Styled
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.provider.Provider
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.HashSet
import kotlin.collections.Iterable
import kotlin.collections.Iterator
import kotlin.collections.Map
import kotlin.collections.emptyMap
import kotlin.collections.forEach
import kotlin.collections.plus
import kotlin.collections.removeAll
import kotlin.collections.set
/**
* A display group which allows both named and unnamed children
*/
class VisualGroup(
override val parent: VisualObject? = null, meta: Meta = EmptyMeta
override val parent: VisualObject? = null, tagRefs: Array<out Meta> = emptyArray()
) : VisualObject, Iterable<VisualObject>, Provider {
private val namedChildren = HashMap<Name, VisualObject>()
private val unnamedChildren = ArrayList<VisualObject>()
override val defaultTarget: String get() = VisualObject.TYPE
override val properties: Styled = Styled(meta)
override val config = Config()
override val properties: Laminate by lazy { combineProperties(parent, config, tagRefs) }
override fun iterator(): Iterator<VisualObject> = (namedChildren.values + unnamedChildren).iterator()
override fun provideTop(target: String): Map<Name, Any> {
return when(target){
return when (target) {
VisualObject.TYPE -> namedChildren
else -> emptyMap()
}
@ -49,13 +61,12 @@ class VisualGroup(
}
/**
*
* Add named or unnamed child to the group. If key is [null] the child is considered unnamed. Both key and value are not
* allowed to be null in the same time. If name is present and [child] is null, the appropriate element is removed.
*/
operator fun set(key: String?, child: VisualObject?) {
if(key == null){
} else {
val name = key.toName()
operator fun set(name: Name?, child: VisualObject?) {
when {
name != null -> {
if (child == null) {
namedChildren.remove(name)
} else {
@ -63,7 +74,12 @@ class VisualGroup(
}
listeners.forEach { it.callback(name, child) }
}
child != null -> unnamedChildren.add(child)
else -> error("Both key and child element are empty")
}
}
operator fun set(key: String?, child: VisualObject?) = set(key?.asName(), child)
/**
* Append unnamed child

View File

@ -2,24 +2,34 @@ package hep.dataforge.vis.common
import hep.dataforge.meta.*
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.provider.Type
import hep.dataforge.vis.common.VisualObject.Companion.META_KEY
import hep.dataforge.vis.common.VisualObject.Companion.TAGS_KEY
import hep.dataforge.vis.common.VisualObject.Companion.TYPE
private fun Laminate.withTop(meta: Meta): Laminate = Laminate(listOf(meta) + layers)
private fun Laminate.withBottom(meta: Meta): Laminate = Laminate(layers + meta)
/**
* A root type for display hierarchy
*/
@Type(TYPE)
interface VisualObject : MetaRepr {
interface VisualObject : MetaRepr, Configurable {
/**
* The parent object of this one. If null, this one is a root.
*/
val parent: VisualObject?
val properties: Styled
/**
* Individual properties configurator
*/
override val config: Config
/**
* All properties including inherited ones
*/
val properties: Laminate
override fun toMeta(): Meta = buildMeta {
"type" to this::class
@ -37,18 +47,11 @@ interface VisualObject : MetaRepr {
}
}
/**
* Get the property of this display object of parent's if not found
*/
tailrec fun VisualObject.getProperty(name: Name): MetaItem<*>? = properties[name] ?: parent?.getProperty(name)
fun VisualObject.getProperty(name: String): MetaItem<*>? = getProperty(name.toName())
/**
* A change listener for [VisualObject] configuration.
*/
fun VisualObject.onChange(owner: Any?, action: (Name, before: MetaItem<*>?, after: MetaItem<*>?) -> Unit) {
properties.onChange(owner, action)
config.onChange(owner, action)
parent?.onChange(owner, action)
}
@ -56,7 +59,7 @@ fun VisualObject.onChange(owner: Any?, action: (Name, before: MetaItem<*>?, afte
* Remove all meta listeners with matching owners
*/
fun VisualObject.removeChangeListener(owner: Any?) {
properties.removeListener(owner)
config.removeListener(owner)
parent?.removeChangeListener(owner)
}
@ -64,18 +67,28 @@ fun VisualObject.removeChangeListener(owner: Any?) {
/**
* Additional meta not relevant to display
*/
val VisualObject.meta: Meta get() = properties[META_KEY]?.node ?: EmptyMeta
val VisualObject.meta: Meta get() = config[META_KEY]?.node ?: EmptyMeta
val VisualObject.tags: List<String> get() = properties[TAGS_KEY].stringList
val VisualObject.tags: List<String> get() = config[TAGS_KEY].stringList
/**
* Basic [VisualObject] leaf element
*/
open class DisplayLeaf(
override val parent: VisualObject?,
meta: Meta = EmptyMeta
open class VisualLeaf(
final override val parent: VisualObject?,
tagRefs: Array<out Meta>
) : VisualObject {
final override val properties = Styled(meta)
final override val config = Config()
override val properties: Laminate by lazy { combineProperties(parent, config, tagRefs) }
}
internal fun combineProperties(parent: VisualObject?, config: Config, tagRefs: Array<out Meta>): Laminate {
val list = ArrayList<Meta>(tagRefs.size + 2)
list += config
list.addAll(tagRefs)
parent?.properties?.let { list.add(it) }
return Laminate(list)
}
///**

View File

@ -2,13 +2,16 @@ package hep.dataforge.vis.common
import hep.dataforge.meta.*
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.names.NameToken
import hep.dataforge.names.asName
import hep.dataforge.values.Value
import kotlin.jvm.JvmName
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
fun String.asName() = NameToken(this).asName()
/**
* A delegate for display object properties
*/
@ -18,17 +21,17 @@ class DisplayObjectDelegate(
val inherited: Boolean
) : ReadWriteProperty<VisualObject, MetaItem<*>?> {
override fun getValue(thisRef: VisualObject, property: KProperty<*>): MetaItem<*>? {
val name = key ?: property.name.toName()
val name = key ?: property.name.asName()
return if (inherited) {
thisRef.getProperty(name)
} else {
thisRef.properties[name]
} else {
thisRef.config[name]
} ?: default
}
override fun setValue(thisRef: VisualObject, property: KProperty<*>, value: MetaItem<*>?) {
val name = key ?: property.name.toName()
thisRef.properties[name] = value
val name = key ?: property.name.asName()
thisRef.config[name] = value
}
}
@ -43,70 +46,71 @@ class DisplayObjectDelegateWrapper<T>(
//private var cachedName: Name? = null
override fun getValue(thisRef: VisualObject, property: KProperty<*>): T {
val name = key ?: property.name.toName()
val name = key ?: property.name.asName()
return if (inherited) {
read(thisRef.getProperty(name))
} else {
read(thisRef.properties[name])
} else {
read(thisRef.config[name])
} ?: default
}
override fun setValue(thisRef: VisualObject, property: KProperty<*>, value: T) {
val name = key ?: property.name.toName()
thisRef.properties[name] = value
val name = key ?: property.name.asName()
thisRef.config[name] = value
}
}
fun VisualObject.value(default: Value? = null, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.value }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.value }
fun VisualObject.string(default: String? = null, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.string }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.string }
fun VisualObject.boolean(default: Boolean? = null, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.boolean }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.boolean }
fun VisualObject.number(default: Number? = null, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.number }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.number }
fun VisualObject.double(default: Double? = null, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.double }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.double }
fun VisualObject.int(default: Int? = null, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.int }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.int }
fun VisualObject.node(key: String? = null, inherited: Boolean = true) =
DisplayObjectDelegateWrapper(key?.toName(), null, inherited) { it.node }
DisplayObjectDelegateWrapper(key?.asName(), null, inherited) { it.node }
fun VisualObject.item(key: String? = null, inherited: Boolean = true) =
DisplayObjectDelegateWrapper(key?.toName(), null, inherited) { it }
DisplayObjectDelegateWrapper(key?.asName(), null, inherited) { it }
//fun <T : Configurable> Configurable.spec(spec: Specification<T>, key: String? = null) = ChildConfigDelegate<T>(key) { spec.wrap(this) }
@JvmName("safeString")
fun VisualObject.string(default: String, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.string }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.string }
@JvmName("safeBoolean")
fun VisualObject.boolean(default: Boolean, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.boolean }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.boolean }
@JvmName("safeNumber")
fun VisualObject.number(default: Number, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.number }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.number }
@JvmName("safeDouble")
fun VisualObject.double(default: Double, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.double }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.double }
@JvmName("safeInt")
fun VisualObject.int(default: Int, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { it.int }
DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.int }
inline fun <reified E : Enum<E>> VisualObject.enum(default: E, key: String? = null, inherited: Boolean = false) =
DisplayObjectDelegateWrapper(key?.toName(), default, inherited) { item -> item.string?.let { enumValueOf<E>(it) } }
DisplayObjectDelegateWrapper(key?.let{ NameToken(it).asName()}, default, inherited) { item -> item.string?.let { enumValueOf<E>(it) } }
//merge properties
@ -116,11 +120,11 @@ fun <T> VisualObject.merge(
): ReadOnlyProperty<VisualObject, T> {
return object : ReadOnlyProperty<VisualObject, T> {
override fun getValue(thisRef: VisualObject, property: KProperty<*>): T {
val name = key?.toName() ?: property.name.toName()
val name = key?.asName() ?: property.name.asName()
val sequence = sequence<MetaItem<*>> {
var thisObj: VisualObject? = thisRef
while (thisObj != null) {
thisObj.properties[name]?.let { yield(it) }
thisObj.config[name]?.let { yield(it) }
thisObj = thisObj.parent
}
}

View File

@ -4,7 +4,6 @@ import hep.dataforge.meta.*
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.vis.common.VisualObject
import hep.dataforge.vis.common.getProperty
import hep.dataforge.vis.common.onChange
import javafx.beans.binding.ObjectBinding
import tornadofx.*
@ -24,7 +23,7 @@ class DisplayObjectFXListener(val obj: VisualObject) {
operator fun get(key: Name): ObjectBinding<MetaItem<*>?> {
return binndings.getOrPut(key) {
object : ObjectBinding<MetaItem<*>?>() {
override fun computeValue(): MetaItem<*>? = obj.getProperty(key)
override fun computeValue(): MetaItem<*>? = obj.properties[key]
}
}
}

View File

@ -27,21 +27,14 @@ class RendererDemoView : View() {
renderer.render {
group = group {
box {
xSize = 100.0
ySize = 100.0
zSize = 100.0
}
box {
box(100,100,100)
box(100,100,100) {
x = 110.0
xSize = 100.0
ySize = 100.0
zSize = 100.0
}
}
}
var color by group.properties.number(1530).int
var color by group.config.number(1530).int
GlobalScope.launch {
val random = Random(111)

View File

@ -11,28 +11,57 @@ import kotlin.math.cos
import kotlin.math.sin
private fun VisualObject.applyPosition(pos: GDMLPosition): VisualObject = apply {
private fun VisualObject.withPosition(
pos: GDMLPosition? = null,
rotation: GDMLRotation? = null,
scale: GDMLScale? = null
): VisualObject =
apply {
// if( this is VisualObject3D){
// pos?.let {
// x = pos.x
// y = pos.y
// z = pos.z
// }
// rotation?.let {
// rotationX = rotation.x
// rotationY = rotation.y
// rotationZ = rotation.z
// }
// } else {
pos?.let {
x = pos.x
y = pos.y
z = pos.z
//TODO convert units if needed
}
private fun VisualObject.applyRotation(rotation: GDMLRotation): VisualObject = apply {
}
rotation?.let {
rotationX = rotation.x
rotationY = rotation.y
rotationZ = rotation.z
}
//}
scale?.let {
scaleX = scale.x
scaleY = scale.y
scaleZ = scale.z
}
//TODO convert units if needed
}
}
private fun VisualGroup.addSolid(root: GDML, solid: GDMLSolid, block: VisualObject.() -> Unit = {}): VisualObject {
private fun VisualGroup.addSolid(
root: GDML,
solid: GDMLSolid,
name: String? = null,
block: VisualObject.() -> Unit = {}
): VisualObject {
return when (solid) {
is GDMLBox -> box(solid.x, solid.y, solid.z)
is GDMLTube -> cylinder(solid.rmax, solid.z) {
is GDMLBox -> box(solid.x, solid.y, solid.z, name)
is GDMLTube -> cylinder(solid.rmax, solid.z, name) {
startAngle = solid.startphi
angle = solid.deltaphi
}
is GDMLXtru -> extrude {
is GDMLXtru -> extrude(name) {
shape {
solid.vertices.forEach {
point(it.x, it.y)
@ -46,6 +75,7 @@ private fun VisualGroup.addSolid(root: GDML, solid: GDMLSolid, block: VisualObje
//Add solid with modified scale
val innerSolid = solid.solidref.resolve(root)
?: error("Solid with tag ${solid.solidref.ref} for scaled solid ${solid.name} not defined")
addSolid(root, innerSolid) {
block()
scaleX = scaleX.toDouble() * solid.scale.x.toDouble()
@ -53,12 +83,12 @@ private fun VisualGroup.addSolid(root: GDML, solid: GDMLSolid, block: VisualObje
scaleZ = scaleZ.toDouble() * solid.scale.z.toDouble()
}
}
is GDMLSphere -> sphere(solid.rmax, solid.deltaphi, solid.deltatheta) {
is GDMLSphere -> sphere(solid.rmax, solid.deltaphi, solid.deltatheta, name) {
phiStart = solid.startphi.toDouble()
thetaStart = solid.starttheta.toDouble()
}
is GDMLOrb -> sphere(solid.r)
is GDMLPolyhedra -> extrude {
is GDMLOrb -> sphere(solid.r, name = name)
is GDMLPolyhedra -> extrude(name) {
//getting the radius of first
require(solid.planes.size > 1) { "The polyhedron geometry requires at least two planes" }
val baseRadius = solid.planes.first().rmax.toDouble()
@ -81,14 +111,13 @@ private fun VisualGroup.addSolid(root: GDML, solid: GDMLSolid, block: VisualObje
is GDMLSubtraction -> CompositeType.SUBTRACT
is GDMLIntersection -> CompositeType.INTERSECT
}
return composite(type) {
return composite(type, name) {
addSolid(root, first) {
solid.resolveFirstPosition(root)?.let { applyPosition(it) }
solid.resolveFirstRotation(root)?.let { applyRotation(it) }
withPosition(solid.resolveFirstPosition(root), solid.resolveFirstRotation(root), null)
}
addSolid(root, second)
solid.resolvePosition(root)?.let { applyPosition(it) }
solid.resolveRotation(root)?.let { applyRotation(it) }
withPosition(solid.resolvePosition(root), solid.resolveRotation(root), null)
}
}
}.apply(block)
@ -97,30 +126,42 @@ private fun VisualGroup.addSolid(root: GDML, solid: GDMLSolid, block: VisualObje
private fun VisualGroup.addVolume(
root: GDML,
group: GDMLGroup,
position: GDMLPosition? = null,
rotation: GDMLRotation? = null,
scale: GDMLScale? = null,
resolveColor: GDMLMaterial.() -> Meta
): VisualGroup {
) {
group(group.name) {
withPosition(position, rotation, scale)
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) {
addSolid(root, solid, solid.name) {
color(material.resolveColor())
}
//TODO render placements
}
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) }
group.physVolumes.forEach { physVolume ->
val volume: GDMLGroup = physVolume.volumeref.resolve(root)
?: error("Volume with ref ${physVolume.volumeref.ref} could not be resolved")
addVolume(
root,
volume,
physVolume.resolvePosition(root),
physVolume.resolveRotation(root),
physVolume.resolveScale(root),
resolveColor
)
}
}
return this
}

View File

@ -17,7 +17,7 @@ import scientifik.gdml.GDML
import kotlin.browser.document
import kotlin.dom.clear
class GDMLDemoApp : ApplicationBase() {
private class GDMLDemoApp : ApplicationBase() {
/**
@ -56,13 +56,13 @@ class GDMLDemoApp : ApplicationBase() {
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 canvas = document.getElementById("canvas") ?: error("Element with id canvas not found on page")
val action: suspend (String) -> Unit = {
canvas.clear()
val output = three.output(canvas)
val gdml = GDML.format.parse(GDML.serializer(), it)
val visual = gdml.toVisual()
output.render(visual)

View File

@ -13,7 +13,7 @@ class BMNTest {
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()){
val stream = if (file.exists()) {
file.inputStream()
} else {
url.openStream()
@ -21,7 +21,7 @@ class BMNTest {
val xmlReader = StAXReader(stream, "UTF-8")
val xml = GDML.format.parse(GDML.serializer(), xmlReader)
repeat(5) {
repeat(20) {
xml.toVisual()
}
}

View File

@ -17,7 +17,7 @@ import kotlin.math.sin
import kotlin.random.Random
class ThreeDemoApp : ApplicationBase() {
private class ThreeDemoApp : ApplicationBase() {
override val stateKeys: List<String> = emptyList()
@ -39,18 +39,12 @@ class ThreeDemoApp : ApplicationBase() {
demo("dynamic", "Dynamic properties") {
val group = group {
box {
box(100, 100, 100) {
z = 110.0
xSize = 100.0
ySize = 100.0
zSize = 100.0
}
box {
box(100, 100, 100) {
visible = false
x = 110.0
xSize = 100.0
ySize = 100.0
zSize = 100.0
//override color for this cube
color(1530)
@ -63,7 +57,7 @@ class ThreeDemoApp : ApplicationBase() {
}
}
var material by group.properties.number(1530).int
var material by group.config.number(1530).int
GlobalScope.launch {
val random = Random(111)
@ -100,34 +94,28 @@ class ThreeDemoApp : ApplicationBase() {
demo("CSG", "CSG operations") {
composite(CompositeType.UNION) {
box(100, 100, 100) {
z = 100
rotationX = PI / 4
rotationY = PI / 4
z = 50
}
box(100, 100, 100)
sphere(50)
color {
"color" to Colors.lightgreen
"opacity" to 0.3
}
}
composite(CompositeType.INTERSECT) {
box(100, 100, 100) {
z = 100
rotationX = PI / 4
rotationY = PI / 4
}
box(100, 100, 100)
y = 300
box(100, 100, 100) {
z = 50
}
sphere(50)
color(Colors.red)
}
composite(CompositeType.SUBTRACT) {
box(100, 100, 100) {
z = 100
rotationX = PI / 4
rotationY = PI / 4
}
box(100, 100, 100)
y = -300
box(100, 100, 100) {
z = 50
}
sphere(50)
color(Colors.blue)
}
}

View File

@ -11,9 +11,9 @@ object ThreeCylinderFactory : MeshThreeFactory<Cylinder>(Cylinder::class) {
return obj.detail?.let {
val segments = it.toDouble().pow(0.5).toInt()
CylinderBufferGeometry(
radiusTop = obj.upperRadius!!,
radiusBottom = obj.radius!!,
height = obj.height!!,
radiusTop = obj.upperRadius,
radiusBottom = obj.radius,
height = obj.height,
radialSegments = segments,
heightSegments = segments,
openEnded = false,
@ -21,9 +21,9 @@ object ThreeCylinderFactory : MeshThreeFactory<Cylinder>(Cylinder::class) {
thetaLength = obj.angle
)
} ?: CylinderBufferGeometry(
radiusTop = obj.upperRadius!!,
radiusBottom = obj.radius!!,
height = obj.height!!,
radiusTop = obj.upperRadius,
radiusBottom = obj.radius,
height = obj.height,
openEnded = false,
thetaStart = obj.startAngle,
thetaLength = obj.angle

View File

@ -1,11 +1,10 @@
package hep.dataforge.vis.spatial.three
import hep.dataforge.meta.boolean
import hep.dataforge.meta.get
import hep.dataforge.names.startsWith
import hep.dataforge.names.toName
import hep.dataforge.provider.Type
import hep.dataforge.vis.common.VisualObject
import hep.dataforge.vis.common.getProperty
import hep.dataforge.vis.common.onChange
import hep.dataforge.vis.spatial.*
import hep.dataforge.vis.spatial.three.ThreeFactory.Companion.TYPE
@ -19,7 +18,7 @@ import info.laht.threekt.objects.LineSegments
import info.laht.threekt.objects.Mesh
import kotlin.reflect.KClass
internal val VisualObject.material get() = getProperty("material").material()
internal val VisualObject.material get() = properties["material"].material()
/**
* Builder and updater for three.js object
@ -43,14 +42,14 @@ interface ThreeFactory<T : VisualObject> {
val mesh = Mesh(geometry, obj.material)
//inherited edges definition, enabled by default
if (obj.getProperty("edges.enabled").boolean != false) {
val material = obj.getProperty("edges.material")?.material() ?: Materials.DEFAULT
if (obj.properties["edges.enabled"].boolean != false) {
val material = obj.properties["edges.material"]?.material() ?: Materials.DEFAULT
mesh.add(LineSegments(EdgesGeometry(mesh.geometry as BufferGeometry), material))
}
//inherited wireframe definition, disabled by default
if (obj.getProperty("wireframe.enabled").boolean == true) {
val material = obj.getProperty("edges.material")?.material() ?: Materials.DEFAULT
if (obj.properties["wireframe.enabled"].boolean == true) {
val material = obj.properties["edges.material"]?.material() ?: Materials.DEFAULT
mesh.add(LineSegments(WireframeGeometry(mesh.geometry as BufferGeometry), material))
}
@ -63,9 +62,9 @@ interface ThreeFactory<T : VisualObject> {
//updated material
mesh.material = obj.material
} else if (
name.startsWith("pos".toName()) ||
name.startsWith("scale".toName()) ||
name.startsWith("rotation".toName()) ||
name.startsWith(PropertyNames3D.position) ||
name.startsWith(PropertyNames3D.rotation) ||
name.startsWith(PropertyNames3D.scale) ||
name.toString() == "visible"
) {
//update position of mesh using this object

View File

@ -38,7 +38,14 @@ class ThreePlugin : AbstractPlugin() {
fun buildObject3D(obj: VisualObject): Object3D {
return when (obj) {
is VisualGroup -> Group(obj.map { buildObject3D(it) }).apply {
is VisualGroup -> Group(obj.mapNotNull {
try {
buildObject3D(it)
} catch (ex: Throwable){
logger.error(ex){"Failed to render $it"}
null
}
}).apply {
updatePosition(obj)
}
is Composite -> compositeFactory(obj)

View File

@ -1,23 +1,18 @@
package hep.dataforge.vis.spatial
import hep.dataforge.meta.EmptyMeta
import hep.dataforge.meta.Meta
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.DisplayLeaf
import hep.dataforge.vis.common.VisualObject
import hep.dataforge.vis.common.double
class Box(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta), Shape {
var xSize by double(100.0)
var ySize by double(100.0)
var zSize by double(100.0)
class Box(parent: VisualObject?, val xSize: Number, val ySize: Number, val zSize: Number, meta: Array<out Meta>) :
VisualObject3D(parent, meta), Shape {
//TODO add helper for color configuration
override fun <T : Any> toGeometry(geometryBuilder: GeometryBuilder<T>) {
val dx = xSize / 2
val dy = ySize / 2
val dz = zSize / 2
val dx = xSize.toDouble() / 2
val dy = ySize.toDouble() / 2
val dz = zSize.toDouble() / 2
val node1 = Point3D(-dx, -dy, -dz)
val node2 = Point3D(dx, -dy, -dz)
val node3 = Point3D(dx, dy, -dz)
@ -40,12 +35,15 @@ class Box(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta), Shape
}
}
fun VisualGroup.box(meta: Meta = EmptyMeta, action: Box.() -> Unit = {}) =
Box(this, meta).apply(action).also { add(it) }
//fun VisualGroup.box(meta: Meta = EmptyMeta, action: Box.() -> Unit = {}) =
// Box(this, meta).apply(action).also { add(it) }
fun VisualGroup.box(xSize: Number, ySize: Number, zSize: Number, meta: Meta = EmptyMeta, action: Box.() -> Unit = {}) =
Box(this, meta).apply(action).apply{
this.xSize = xSize.toDouble()
this.ySize = ySize.toDouble()
this.zSize = zSize.toDouble()
}.also { add(it) }
fun VisualGroup.box(
xSize: Number,
ySize: Number,
zSize: Number,
name: String? = null,
vararg meta: Meta,
action: Box.() -> Unit = {}
) =
Box(this, xSize, ySize, zSize, meta).apply(action).also { set(name, it) }

View File

@ -1,9 +1,8 @@
package hep.dataforge.vis.spatial
import hep.dataforge.meta.EmptyMeta
import hep.dataforge.meta.Meta
import hep.dataforge.meta.seal
import hep.dataforge.vis.common.DisplayLeaf
import hep.dataforge.meta.update
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.VisualObject
@ -13,28 +12,35 @@ enum class CompositeType {
SUBTRACT
}
class Composite(
open class Composite(
parent: VisualObject?,
val first: VisualObject,
val second: VisualObject,
val type: CompositeType = CompositeType.UNION,
meta: Meta = EmptyMeta
) : DisplayLeaf(parent, meta)
meta: Array<out Meta>
) : VisualObject3D(parent, meta)
fun VisualGroup.composite(type: CompositeType, builder: VisualGroup.() -> Unit): Composite {
fun VisualGroup.composite(
type: CompositeType,
name: String? = null,
vararg meta: Meta,
builder: VisualGroup.() -> Unit
): Composite {
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 {
this.add(it)
val groupMeta = group.properties.seal()
return Composite(this, children[0], children[1], type, meta).also {
it.config.update(groupMeta)
set(name, it)
}
}
fun VisualGroup.union(builder: VisualGroup.() -> Unit) =
composite(CompositeType.UNION,builder)
fun VisualGroup.union(name: String? = null, vararg meta: Meta, builder: VisualGroup.() -> Unit) =
composite(CompositeType.UNION, name, *meta, builder = builder)
fun VisualGroup.subtract(builder: VisualGroup.() -> Unit) =
composite(CompositeType.SUBTRACT,builder)
fun VisualGroup.subtract(name: String? = null, vararg meta: Meta, builder: VisualGroup.() -> Unit) =
composite(CompositeType.SUBTRACT, name, *meta, builder = builder)
fun VisualGroup.intersect(builder: VisualGroup.() -> Unit) =
composite(CompositeType.INTERSECT,builder)
fun VisualGroup.intersect(name: String? = null, vararg meta: Meta, builder: VisualGroup.() -> Unit) =
composite(CompositeType.INTERSECT, name, *meta, builder = builder)

View File

@ -1,26 +1,19 @@
package hep.dataforge.vis.spatial
import hep.dataforge.meta.*
import hep.dataforge.meta.Meta
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.DisplayLeaf
import hep.dataforge.vis.common.VisualLeaf
import hep.dataforge.vis.common.VisualObject
class Convex(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta) {
class Convex(parent: VisualObject?, val points: List<Point3D>, meta: Array<out Meta>) : VisualLeaf(parent, meta) {
val points = points(properties["points"] ?: error("Vertices not defined"))
companion object {
const val TYPE = "geometry.3d.convex"
fun points(item: MetaItem<*>): List<Point3D> {
return item.node?.getAll("point")?.map { (_, value) ->
Point3D.from(value.node?: error("Point definition is not a node"))
} ?: emptyList()
}
}
}
fun VisualGroup.convex(meta: Meta = EmptyMeta, action: ConvexBuilder.() -> Unit = {}) =
fun VisualGroup.convex(vararg meta: Meta, action: ConvexBuilder.() -> Unit = {}) =
ConvexBuilder().apply(action).build(this, meta).also { add(it) }
class ConvexBuilder {
@ -30,13 +23,7 @@ class ConvexBuilder {
points.add(Point3D(x, y, z))
}
fun build(parent: VisualObject?, meta: Meta): Convex {
val points = buildMeta {
points.forEachIndexed { index, value ->
"points.point[$index]" to value.toMeta()
}
}.seal()
return Convex(parent, points)
fun build(parent: VisualObject?, meta: Array<out Meta>): Convex {
return Convex(parent, points, meta)
}
}

View File

@ -1,9 +1,8 @@
package hep.dataforge.vis.spatial
import hep.dataforge.meta.EmptyMeta
import hep.dataforge.meta.Meta
import hep.dataforge.vis.common.DisplayLeaf
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.VisualLeaf
import hep.dataforge.vis.common.VisualObject
import hep.dataforge.vis.common.number
import kotlin.math.PI
@ -11,18 +10,23 @@ import kotlin.math.PI
/**
* A cylinder or cut cone segment
*/
class Cylinder(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta) {
var radius by number()
var upperRadius by number(default = radius)
var height by number()
class Cylinder(parent: VisualObject?, radius: Number, height: Number, meta: Array<out Meta>) :
VisualLeaf(parent, meta) {
var radius by number(radius)
var upperRadius by number(radius)
var height by number(height)
var startAngle by number(0.0)
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)
cylinder.radius = r
cylinder.height = height
fun VisualGroup.cylinder(
r: Number,
height: Number,
name: String? = null,
vararg meta: Meta,
block: Cylinder.() -> Unit = {}
): Cylinder {
val cylinder = Cylinder(this, r, height, meta)
cylinder.apply(block)
return cylinder.also { add(it) }
return cylinder.also { set(name, it) }
}

View File

@ -1,8 +1,8 @@
package hep.dataforge.vis.spatial
import hep.dataforge.meta.*
import hep.dataforge.vis.common.DisplayLeaf
import hep.dataforge.meta.Meta
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.VisualLeaf
import hep.dataforge.vis.common.VisualObject
import kotlin.math.PI
import kotlin.math.cos
@ -18,7 +18,7 @@ class Shape2DBuilder {
list.add(Point2D(x, y))
}
infix fun Number.to(y:Number) = point(this, y)
infix fun Number.to(y: Number) = point(this, y)
fun build(): Shape2D = list
}
@ -31,45 +31,22 @@ fun Shape2DBuilder.polygon(vertices: Int, radius: Number) {
}
}
class Layer(override val config: Config) : Specific {
var z by number(0.0)
var x by number(0.0)
var y by number(0.0)
var scale by number(1.0)
data class Layer(var x: Number, var y: Number, var z: Number, var scale: Number)
companion object : Specification<Layer> {
override fun wrap(config: Config): Layer = Layer(config)
}
}
class Extruded(parent: VisualObject?, meta: Array<out Meta>) : VisualLeaf(parent, meta), Shape {
//class Layer(val z: Number, val x: Number = 0.0, val y: Number = 0.0, val scale: Number = 1.0)
class Extruded(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta), Shape {
val shape
get() = properties.getAll("shape.point").map { (_, value) ->
Point2D.from(value.node ?: error("Point definition is not a node"))
}
var shape: List<Point2D> = ArrayList()
fun shape(block: Shape2DBuilder.() -> Unit) {
val points = Shape2DBuilder().apply(block).build().map { it.toMeta() }
properties["shape.point"] = points
this.shape = Shape2DBuilder().apply(block).build()
//TODO send invalidation signal
}
val layers
get() = properties.getAll("layer").values.map {
Layer.wrap(it.node ?: error("layer item is not a node"))
}
val layers: MutableList<Layer> = ArrayList()
fun layer(z: Number, x: Number = 0.0, y: Number = 0.0, scale: Number = 1.0): Layer {
val layer = Layer.build {
this.x = x
this.y = y
this.z = z
this.scale = scale
}
properties.append("layer", layer)
return layer
fun layer(z: Number, x: Number = 0.0, y: Number = 0.0, scale: Number = 1.0) {
layers.add(Layer(x,y,z,scale))
//TODO send invalidation signal
}
override fun <T : Any> toGeometry(geometryBuilder: GeometryBuilder<T>) {
@ -121,5 +98,5 @@ class Extruded(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta), S
}
}
fun VisualGroup.extrude(meta: Meta = EmptyMeta, action: Extruded.() -> Unit = {}) =
Extruded(this, meta).apply(action).also { add(it) }
fun VisualGroup.extrude(name: String? = null, vararg meta: Meta, action: Extruded.() -> Unit = {}) =
Extruded(this, meta).apply(action).also { set(name, it) }

View File

@ -1,14 +1,13 @@
package hep.dataforge.vis.spatial
import hep.dataforge.meta.EmptyMeta
import hep.dataforge.meta.Meta
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.DisplayLeaf
import hep.dataforge.vis.common.VisualLeaf
import hep.dataforge.vis.common.VisualObject
import hep.dataforge.vis.common.double
import kotlin.math.PI
class Sphere(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta) {
class Sphere(parent: VisualObject?, meta: Array<out Meta>) : VisualLeaf(parent, meta) {
var radius by double(50.0)
var phiStart by double(0.0)
var phi by double(2 * PI)
@ -16,17 +15,15 @@ class Sphere(parent: VisualObject?, meta: Meta) : DisplayLeaf(parent, meta) {
var theta by double(PI)
}
fun VisualGroup.sphere(meta: Meta = EmptyMeta, action: Sphere.() -> Unit = {}) =
Sphere(this, meta).apply(action).also { add(it) }
fun VisualGroup.sphere(
radius: Number,
phi: Number = 2 * PI,
theta: Number = PI,
meta: Meta = EmptyMeta,
name: String? = null,
vararg meta: Meta,
action: Sphere.() -> Unit = {}
) = Sphere(this, meta).apply(action).apply {
this.radius = radius.toDouble()
this.phi = phi.toDouble()
this.theta = theta.toDouble()
}.also { add(it) }
}.also { set(name, it) }

View File

@ -0,0 +1,196 @@
package hep.dataforge.vis.spatial
import hep.dataforge.meta.*
import hep.dataforge.names.plus
import hep.dataforge.output.Output
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.VisualLeaf
import hep.dataforge.vis.common.VisualObject
import hep.dataforge.vis.common.asName
/**
* Performance optimized version of visual object
*/
open class VisualObject3D(parent: VisualObject?, tagRefs: Array<out Meta>) : VisualLeaf(parent, tagRefs) {
var x: Number? = null; get() = field ?: (this as VisualLeaf).x
var y: Number? = null; get() = field ?: (this as VisualLeaf).y
var z: Number? = null; get() = field ?: (this as VisualLeaf).z
var rotationX: Number? = null; get() = field ?: (this as VisualLeaf).rotationX
var rotationY: Number? = null; get() = field ?: (this as VisualLeaf).rotationY
var rotationZ: Number? = null; get() = field ?: (this as VisualLeaf).rotationZ
}
fun VisualGroup.group(key: String? = null, vararg meta: Meta, action: VisualGroup.() -> Unit = {}): VisualGroup =
VisualGroup(this, meta).apply(action).also { set(key, it) }
fun Output<VisualObject>.render(meta: Meta = EmptyMeta, action: VisualGroup.() -> Unit) =
render(VisualGroup().apply(action), meta)
//TODO replace properties by containers?
object PropertyNames3D {
val x = "x".asName()
val y = "y".asName()
val z = "z".asName()
val position = "pos".asName()
val xPos = position + x
val yPos = position + y
val zPos = position + z
val rotation = "rotation".asName()
val xRotation = rotation + x
val yRotation = rotation + y
val zRotation = rotation + z
val rotationOrder = rotation + "order"
val scale = "scale".asName()
val xScale = scale + x
val yScale = scale + y
val zScale = scale + z
}
// Common properties
/**
* Visibility property. Inherited from parent
*/
var VisualObject.visible
get() = properties["visible"].boolean ?: true
set(value) {
config["visible"] = value
}
// 3D Object position
/**
* x position property relative to parent. Not inherited
*/
var VisualObject.x
get() = config[PropertyNames3D.xPos].number ?: 0.0
set(value) {
config[PropertyNames3D.xPos] = value
}
/**
* y position property. Not inherited
*/
var VisualObject.y
get() = config[PropertyNames3D.yPos].number ?: 0.0
set(value) {
config[PropertyNames3D.yPos] = value
}
/**
* z position property. Not inherited
*/
var VisualObject.z
get() = config[PropertyNames3D.zPos].number ?: 0.0
set(value) {
config[PropertyNames3D.zPos] = value
}
// 3D Object rotation
/**
* x rotation relative to parent. Not inherited
*/
var VisualObject.rotationX
get() = config[PropertyNames3D.xRotation].number ?: 0.0
set(value) {
config[PropertyNames3D.xRotation] = value
}
/**
* y rotation relative to parent. Not inherited
*/
var VisualObject.rotationY
get() = config[PropertyNames3D.yRotation].number ?: 0.0
set(value) {
config[PropertyNames3D.yRotation] = value
}
/**
* z rotation relative to parent. Not inherited
*/
var VisualObject.rotationZ
get() = config[PropertyNames3D.zRotation].number ?: 0.0
set(value) {
config[PropertyNames3D.zRotation] = value
}
enum class RotationOrder {
XYZ,
YZX,
ZXY,
XZY,
YXZ,
ZYX
}
/**
* Rotation order. Not inherited
*/
var VisualObject.rotationOrder: RotationOrder
get() = config[PropertyNames3D.rotationOrder].enum<RotationOrder>() ?: RotationOrder.XYZ
set(value) {
config[PropertyNames3D.rotationOrder] = value
}
// 3D object scale
/**
* X scale. Not inherited
*/
var VisualObject.scaleX
get() = config[PropertyNames3D.xScale].number ?: 1.0
set(value) {
config[PropertyNames3D.xScale] = value
}
/**
* Y scale. Not inherited
*/
var VisualObject.scaleY
get() = config[PropertyNames3D.yScale].number ?: 1.0
set(value) {
config[PropertyNames3D.yScale] = value
}
/**
* Z scale. Not inherited
*/
var VisualObject.scaleZ
get() = config[PropertyNames3D.zScale].number ?: 1.0
set(value) {
config[PropertyNames3D.zScale] = value
}
//TODO add inherited scale
/**
* Preferred number of polygons for displaying the object. If not defined, uses shape or renderer default
*/
var VisualObject.detail: Int?
get() = properties["detail"]?.int
set(value) {
config["detail"] = value
}
object World {
const val CAMERA_INITIAL_DISTANCE = -500.0
const val CAMERA_INITIAL_X_ANGLE = -50.0
const val CAMERA_INITIAL_Y_ANGLE = 0.0
const val CAMERA_INITIAL_Z_ANGLE = -210.0
const val CAMERA_NEAR_CLIP = 0.1
const val CAMERA_FAR_CLIP = 10000.0
}

View File

@ -1,164 +0,0 @@
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
import hep.dataforge.vis.common.getProperty
fun VisualGroup.group(meta: Meta = EmptyMeta, action: VisualGroup.() -> Unit = {}): VisualGroup =
VisualGroup(this, meta).apply(action).also { add(it) }
fun Output<VisualObject>.render(meta: Meta = EmptyMeta, action: VisualGroup.() -> Unit) =
render(VisualGroup(null, EmptyMeta).apply(action), meta)
//TODO replace properties by containers?
// Common properties
/**
* Visibility property. Inherited from parent
*/
var VisualObject.visible
get() = getProperty("visible").boolean ?: true
set(value) {
properties["visible"] = value
}
// 3D Object position
private val xPos = "pos.x".toName()
/**
* x position property relative to parent. Not inherited
*/
var VisualObject.x
get() = properties[xPos].number ?: 0.0
set(value) {
properties[xPos] = value
}
private val yPos = "pos.y".toName()
/**
* y position property. Not inherited
*/
var VisualObject.y
get() = properties[yPos].number ?: 0.0
set(value) {
properties[yPos] = value
}
private val zPos = "pos.z".toName()
/**
* z position property. Not inherited
*/
var VisualObject.z
get() = properties[zPos].number ?: 0.0
set(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[xRotation].number ?: 0.0
set(value) {
properties[xRotation] = value
}
private val yRotation = "rotation.y".toName()
/**
* y rotation relative to parent. Not inherited
*/
var VisualObject.rotationY
get() = properties[yRotation].number ?: 0.0
set(value) {
properties[yRotation] = value
}
private val zRotation = "rotation.z".toName()
/**
* z rotation relative to parent. Not inherited
*/
var VisualObject.rotationZ
get() = properties[zRotation].number ?: 0.0
set(value) {
properties[zRotation] = value
}
enum class RotationOrder {
XYZ,
YZX,
ZXY,
XZY,
YXZ,
ZYX
}
/**
* Rotation order. Not inherited
*/
var VisualObject.rotationOrder: RotationOrder
get() = getProperty("rotation.order").enum<RotationOrder>() ?: RotationOrder.XYZ
set(value) {
properties["rotation.order"] = value
}
// 3D object scale
/**
* X scale. Not inherited
*/
var VisualObject.scaleX
get() = properties["scale.x"].number ?: 1.0
set(value) {
properties["scale.x"] = value
}
/**
* Y scale. Not inherited
*/
var VisualObject.scaleY
get() = properties["scale.y"].number ?: 1.0
set(value) {
properties["scale.y"] = value
}
/**
* Z scale. Not inherited
*/
var VisualObject.scaleZ
get() = properties["scale.z"].number ?: 1.0
set(value) {
properties["scale.z"] = value
}
//TODO add inherited scale
/**
* Preferred number of polygons for displaying the object. If not defined, uses shape or renderer default
*/
var VisualObject.detail: Int?
get() = properties["detail"]?.int
set(value) {
properties["detail"] = value
}
object World {
const val CAMERA_INITIAL_DISTANCE = -500.0
const val CAMERA_INITIAL_X_ANGLE = -50.0
const val CAMERA_INITIAL_Y_ANGLE = 0.0
const val CAMERA_INITIAL_Z_ANGLE = -210.0
const val CAMERA_NEAR_CLIP = 0.1
const val CAMERA_FAR_CLIP = 10000.0
}

View File

@ -26,7 +26,7 @@ class ConvexTest {
val convex = group.first() as Convex
val pointsNode = convex.properties["points"].node
val pointsNode = convex.config["points"].node
assertEquals(8, pointsNode?.items?.count())
val points = pointsNode?.getAll("point".toName())

View File

@ -0,0 +1,52 @@
package hep.dataforge.vis.spatial
import hep.dataforge.vis.common.Colors
import hep.dataforge.vis.common.VisualGroup
import hep.dataforge.vis.common.color
import kotlin.math.PI
import kotlin.test.Test
import kotlin.test.assertEquals
class GroupTest {
@Test
fun testGroupWithComposite(){
val group = VisualGroup().apply{
union {
box(100, 100, 100) {
z = 100
rotationX = PI / 4
rotationY = PI / 4
}
box(100, 100, 100)
color {
"color" to Colors.lightgreen
"opacity" to 0.3
}
}
intersect{
box(100, 100, 100) {
z = 100
rotationX = PI / 4
rotationY = PI / 4
}
box(100, 100, 100)
y = 300
color(Colors.red)
}
subtract{
box(100, 100, 100) {
z = 100
rotationX = PI / 4
rotationY = PI / 4
}
box(100, 100, 100)
y = -300
color(Colors.blue)
}
}
assertEquals(3, group.count())
assertEquals(300.0,group.toList()[1].y.toDouble())
assertEquals(-300.0,group.toList()[2].y.toDouble())
}
}