Merge pull request #69 from mipt-npm/feature/root

feature/root
This commit is contained in:
Alexander Nozik 2021-09-11 19:14:21 +03:00 committed by GitHub
commit 44c4356794
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 28534 additions and 180 deletions

View File

@ -1,7 +1,7 @@
plugins {
id("ru.mipt.npm.gradle.project")
// kotlin("multiplatform") version "1.5.30-RC" apply false
// kotlin("js") version "1.5.30-RC" apply false
kotlin("multiplatform") version "1.5.30" apply false
kotlin("js") version "1.5.30" apply false
}
val dataforgeVersion by extra("0.5.1")
@ -16,7 +16,7 @@ allprojects {
}
group = "space.kscience"
version = "0.2.0-dev-23"
version = "0.2.0-dev-24"
}
subprojects {
@ -36,7 +36,8 @@ apiValidation {
ignoredPackages.add("info.laht.threekt")
}
//workaround for https://youtrack.jetbrains.com/issue/KT-48273
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin::class.java) {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().versions.webpackDevServer.version = "4.0.0-rc.0"
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().versions.webpackDevServer.version = "4.0.0"
}

View File

@ -0,0 +1,19 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
}
kscience{
useSerialization {
json()
}
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(project(":visionforge-solid"))
}
}
}
}

View File

@ -0,0 +1,139 @@
package ru.mipt.npm.root
import kotlinx.serialization.json.Json
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.misc.Named
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.values.doubleArray
import kotlin.properties.ReadOnlyProperty
public fun MetaProvider.doubleArray(
vararg default: Double,
key: Name? = null,
): ReadOnlyProperty<Any?, DoubleArray> = value(key) {
it?.doubleArray ?: doubleArrayOf(*default)
}
public class DObjectCache(private val cache: List<Meta>, public val refStack: List<Int> = emptyList()) {
public operator fun get(index: Int): Meta = cache[index]
public fun stack(ref: Int): DObjectCache = DObjectCache(cache, refStack + ref)
public companion object {
public val empty: DObjectCache = DObjectCache(emptyList(), emptyList())
}
}
public open class DObject(public val meta: Meta, private val refCache: DObjectCache) {
public val typename: String by meta.string(key = "_typename".asName()) {
error("Type is not defined")
}
private fun <T : DObject> resolve(builder: (Meta, DObjectCache) -> T, meta: Meta): T? {
meta["\$ref"]?.int?.let { refId ->
if (refCache.refStack.contains(refId)) {
println("Circular reference $refId in stack ${refCache.refStack}")
return null
}
return builder(refCache[refId], refCache.stack(refId))
}
return builder(meta, refCache)
}
internal fun <T : DObject> tObjectArray(
builder: (Meta, DObjectCache) -> T
): ReadOnlyProperty<Any?, List<T>> = ReadOnlyProperty { _, property ->
meta.getIndexed(Name.of(property.name, "arr")).values.mapNotNull {
resolve(builder, it)
}
}
internal fun <T : DObject> dObject(
builder: (Meta, DObjectCache) -> T,
key: Name? = null
): ReadOnlyProperty<Any?, T?> = ReadOnlyProperty { _, property ->
meta[key ?: property.name.asName()]?.let { resolve(builder, it) }
}
}
public open class DNamed(meta: Meta, refCache: DObjectCache) : DObject(meta, refCache) {
public val fName: String by meta.string("")
public val fTitle: String by meta.string("")
}
public class DGeoMaterial(meta: Meta, refCache: DObjectCache) : DNamed(meta, refCache) {
}
public class DGeoMedium(meta: Meta, refCache: DObjectCache) : DNamed(meta, refCache) {
public val fMaterial: DGeoMaterial? by dObject(::DGeoMaterial)
public val fParams: DoubleArray by meta.doubleArray()
}
public class DGeoShape(meta: Meta, refCache: DObjectCache) : DNamed(meta, refCache) {
public val fDX: Double by meta.double(0.0)
public val fDY: Double by meta.double(0.0)
public val fDZ: Double by meta.double(0.0)
}
public class DGeoVolume(meta: Meta, refCache: DObjectCache) : DNamed(meta, refCache), Named {
public val fNodes: List<DGeoNode> by tObjectArray(::DGeoNode)
public val fShape: DGeoShape? by dObject(::DGeoShape)
public val fMedium: DGeoMedium? by dObject(::DGeoMedium)
public val fFillColor: Int? by meta.int()
override val name: Name by lazy { Name.parse(fName.ifEmpty { "volume[${meta.hashCode().toUInt()}]" }) }
}
public class DGeoNode(meta: Meta, refCache: DObjectCache) : DNamed(meta, refCache) {
public val fVolume: DGeoVolume? by dObject(::DGeoVolume)
}
public class DGeoMatrix(meta: Meta, refCache: DObjectCache) : DNamed(meta, refCache) {
}
public class DGeoBoolNode(meta: Meta, refCache: DObjectCache) : DObject(meta, refCache) {
public val fLeft: DGeoShape? by dObject(::DGeoShape)
public val fLeftMat: DGeoMatrix? by dObject(::DGeoMatrix)
public val fRight: DGeoShape? by dObject(::DGeoShape)
public val fRightMat: DGeoMatrix? by dObject(::DGeoMatrix)
}
public class DGeoManager(meta: Meta, refCache: DObjectCache) : DNamed(meta, refCache) {
public val fMatrices: List<DGeoMatrix> by tObjectArray(::DGeoMatrix)
public val fShapes: List<DGeoShape> by tObjectArray(::DGeoShape)
public val fVolumes: List<DGeoVolume> by tObjectArray(::DGeoVolume)
public val fNodes: List<DGeoNode> by tObjectArray(::DGeoNode)
public companion object {
public fun parse(string: String): DGeoManager {
val meta = Json.decodeFromString(MetaSerializer, string)
val res = ArrayList<Meta>(4096)
fun fillCache(element: Meta) {
if (element["\$ref"] == null) {
res.add(element)
element.items.values.forEach {
if (!it.isLeaf) {
fillCache(it)
}
}
}
}
fillCache(meta)
val refCache = DObjectCache(res)
return DGeoManager(meta, refCache)
}
}
}

View File

@ -0,0 +1,331 @@
package ru.mipt.npm.root
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.values.doubleArray
import space.kscience.visionforge.isEmpty
import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.SolidMaterial.Companion.MATERIAL_COLOR_KEY
import kotlin.math.*
private val volumesName = Name.EMPTY //"volumes".asName()
private operator fun Number.times(d: Double) = toDouble() * d
private operator fun Number.times(f: Float) = toFloat() * f
private fun degToRad(d: Double) = d * PI / 180.0
private class RootToSolidContext(val prototypeHolder: PrototypeHolder, val maxLayer: Int = 3) {
val layers: MutableList<Int> = mutableListOf(0)
val layerLimits = listOf(10_000, 25_000, 50_000, 100_000, 200_000, 400_000, 600_000)
val bottomLayer: Int get() = layers.size - 1
fun addLayer() {
layers.add(0)
}
}
// converting to XYZ to TaitBryan angles according to https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix
private fun Solid.rotate(rot: DoubleArray) {
val xAngle = atan2(-rot[5], rot[8])
val yAngle = atan2(rot[2], sqrt(1.0 - rot[2].pow(2)))
val zAngle = atan2(-rot[1], rot[0])
rotation = Point3D(xAngle, yAngle, zAngle)
}
private fun Solid.translate(trans: DoubleArray) {
val (x, y, z) = trans
position = Point3D(x, y, z)
}
private fun Solid.useMatrix(matrix: DGeoMatrix?) {
if (matrix == null) return
when (matrix.typename) {
"TGeoIdentity" -> {
//do nothing
}
"TGeoTranslation" -> {
val fTranslation by matrix.meta.doubleArray()
translate(fTranslation)
}
"TGeoRotation" -> {
val fRotationMatrix by matrix.meta.doubleArray()
rotate(fRotationMatrix)
}
"TGeoCombiTrans" -> {
val fTranslation by matrix.meta.doubleArray()
translate(fTranslation)
matrix.meta["fRotation.fRotationMatrix"]?.value?.let {
rotate(it.doubleArray)
}
}
"TGeoHMatrix" -> {
val fTranslation by matrix.meta.doubleArray()
val fRotationMatrix by matrix.meta.doubleArray()
val fScale by matrix.meta.doubleArray()
translate(fTranslation)
rotate(fRotationMatrix)
scale = Point3D(fScale[0], fScale[1], fScale[2])
}
}
}
private fun SolidGroup.addShape(
shape: DGeoShape,
context: RootToSolidContext,
name: String? = shape.fName.ifEmpty { null },
block: Solid.() -> Unit = {}
) {
when (shape.typename) {
"TGeoCompositeShape" -> {
val fNode: DGeoBoolNode? by shape.dObject(::DGeoBoolNode)
val node = fNode ?: error("Composite shape node not resolved")
val compositeType = when (node.typename) {
"TGeoIntersection" -> CompositeType.INTERSECT
"TGeoSubtraction" -> CompositeType.SUBTRACT
"TGeoUnion" -> CompositeType.GROUP
else -> error("Unknown bool node type ${node.typename}")
}
smartComposite(compositeType, name = name) {
addShape(node.fLeft!!, context, null) {
this.useMatrix(node.fLeftMat)
}
addShape(node.fRight!!, context, null) {
this.useMatrix(node.fRightMat)
}
}.apply(block)
}
"TGeoXtru" -> {
val fNvert by shape.meta.int(0)
val fX by shape.meta.doubleArray()
val fY by shape.meta.doubleArray()
val fNz by shape.meta.int(0)
val fZ by shape.meta.doubleArray()
val fX0 by shape.meta.doubleArray()
val fY0 by shape.meta.doubleArray()
val fScale by shape.meta.doubleArray()
extruded(name = name) {
(0 until fNvert).forEach { index ->
shape {
point(fX[index], fY[index])
}
}
(0 until fNz).forEach { index ->
layer(
fZ[index],
fX0[index],
fY0[index],
fScale[index]
)
}
}.apply(block)
}
"TGeoTube" -> {
val fRmax by shape.meta.double(0.0)
val fDz by shape.meta.double(0.0)
val fRmin by shape.meta.double(0.0)
tube(
radius = fRmax,
height = fDz * 2,
innerRadius = fRmin,
name = name,
block = block
)
}
"TGeoTubeSeg" -> {
val fRmax by shape.meta.double(0.0)
val fDz by shape.meta.double(0.0)
val fRmin by shape.meta.double(0.0)
val fPhi1 by shape.meta.double(0.0)
val fPhi2 by shape.meta.double(0.0)
tube(
radius = fRmax,
height = fDz * 2,
innerRadius = fRmin,
startAngle = degToRad(fPhi1),
angle = degToRad(fPhi2 - fPhi1),
name = name,
block = block
)
}
"TGeoPcon" -> {
val fDphi by shape.meta.double(0.0)
val fNz by shape.meta.int(2)
val fPhi1 by shape.meta.double(360.0)
val fRmax by shape.meta.doubleArray()
val fRmin by shape.meta.doubleArray()
val fZ by shape.meta.doubleArray()
if (fNz == 2) {
coneSurface(
bottomOuterRadius = fRmax[0],
bottomInnerRadius = fRmin[0],
height = fZ[1] - fZ[0],
topOuterRadius = fRmax[1],
topInnerRadius = fRmin[1],
startAngle = degToRad(fPhi1),
angle = degToRad(fDphi),
name = name,
) {
z = (fZ[1] + fZ[0]) / 2
}.apply(block)
} else {
TODO()
}
}
"TGeoPgon" -> {
val fDphi by shape.meta.double(0.0)
val fNz by shape.meta.int(2)
val fPhi1 by shape.meta.double(360.0)
val fRmax by shape.meta.doubleArray()
val fRmin by shape.meta.doubleArray()
val fZ by shape.meta.doubleArray()
val fNedges by shape.meta.int(1)
val startphi = degToRad(fPhi1)
val deltaphi = degToRad(fDphi)
extruded(name) {
//getting the radius of first
require(fNz > 1) { "The polyhedron geometry requires at least two planes" }
val baseRadius = fRmax[0]
shape {
(0..fNedges).forEach {
val phi = deltaphi * fNedges * it + startphi
(baseRadius * cos(phi) to baseRadius * sin(phi))
}
}
(0 until fNz).forEach { index ->
//scaling all radii relative to first layer radius
layer(fZ[index], scale = fRmax[index] / baseRadius)
}
}.apply(block)
}
"TGeoShapeAssembly" -> {
val fVolume by shape.dObject(::DGeoVolume)
fVolume?.let { volume ->
addRootVolume(volume, context, block = block)
}
}
"TGeoBBox" -> {
box(shape.fDX * 2, shape.fDY * 2, shape.fDZ * 2, name = name, block = block)
}
else -> {
TODO("A shape with type ${shape.typename} not implemented")
}
}
}
private fun SolidGroup.addRootNode(obj: DGeoNode, context: RootToSolidContext) {
val volume = obj.fVolume ?: return
addRootVolume(volume, context, obj.fName) {
if (context.bottomLayer > 0) {
this.layer = context.bottomLayer
}
when (obj.typename) {
"TGeoNodeMatrix" -> {
val fMatrix by obj.dObject(::DGeoMatrix)
this.useMatrix(fMatrix)
}
"TGeoNodeOffset" -> {
val fOffset by obj.meta.double(0.0)
x = fOffset
}
}
}
}
private fun buildVolume(volume: DGeoVolume, context: RootToSolidContext): Solid? {
val group = SolidGroup {
val nodesNum = volume.fNodes.size
if (nodesNum == 0) {
//TODO add smart filter
volume.fShape?.let { shape ->
addShape(shape, context)
}
}
val expectedLayerSize = context.layers.last() + nodesNum
//If expected number exceeds layer limit, move everything else to the bottom layer.
if (expectedLayerSize >= context.layerLimits[context.bottomLayer]) {
context.addLayer()
println("Adding new layer. Sizes after add: ${context.layers}")
}
context.layers[context.bottomLayer] += nodesNum
volume.fNodes.forEach { node ->
addRootNode(node, context)
}
}
return if (group.isEmpty()) {
null
} else if (group.children.size == 1 && group.meta.isEmpty()) {
(group.children.values.first() as Solid).apply { parent = null }
} else {
group
}
}
//private val SolidGroup.rootPrototypes: SolidGroup get() = (parent as? SolidGroup)?.rootPrototypes ?: this
private fun SolidGroup.addRootVolume(
volume: DGeoVolume,
context: RootToSolidContext,
name: String? = null,
cache: Boolean = true,
block: Solid.() -> Unit = {}
) {
//skip if maximum layer number is reached
if (context.bottomLayer > context.maxLayer){
println("Maximum layer depth reached. Skipping ${volume.fName}")
return
}
val combinedName = if (volume.fName.isEmpty()) {
name
} else if (name == null) {
volume.fName
} else {
"${name}_${volume.fName}"
}
if (!cache) {
val group = buildVolume(volume, context)?.apply {
volume.fFillColor?.let {
meta[MATERIAL_COLOR_KEY] = RootColors[it]
}
block()
}
set(combinedName?.let { Name.parse(it) }, group)
} else {
val templateName = volumesName + volume.name
val existing = getPrototype(templateName)
if (existing == null) {
context.prototypeHolder.prototypes {
val group = buildVolume(volume, context)
set(templateName, group)
}
}
ref(templateName, name).apply {
volume.fFillColor?.let {
meta[MATERIAL_COLOR_KEY] = RootColors[it]
}
block()
}
}
}
public fun DGeoManager.toSolid(): SolidGroup = SolidGroup {
val context = RootToSolidContext(this)
fNodes.forEach { node ->
addRootNode(node, context)
}
}

View File

@ -0,0 +1,42 @@
package ru.mipt.npm.root
public object RootColors {
private val colorMap = Array<String>(924) { "white" }
//colorMap[110] = "white"
private val moreCol = listOf(
11 to "c1b7ad4d4d4d6666668080809a9a9ab3b3b3cdcdcde6e6e6f3f3f3cdc8accdc8acc3c0a9bbb6a4b3a697b8a49cae9a8d9c8f83886657b1cfc885c3a48aa9a1839f8daebdc87b8f9a768a926983976e7b857d9ad280809caca6c0d4cf88dfbb88bd9f83c89a7dc08378cf5f61ac8f94a6787b946971d45a549300ff7b00ff6300ff4b00ff3300ff1b00ff0300ff0014ff002cff0044ff005cff0074ff008cff00a4ff00bcff00d4ff00ecff00fffd00ffe500ffcd00ffb500ff9d00ff8500ff6d00ff5500ff3d00ff2600ff0e0aff0022ff003aff0052ff006aff0082ff009aff00b1ff00c9ff00e1ff00f9ff00ffef00ffd700ffbf00ffa700ff8f00ff7700ff6000ff4800ff3000ff1800ff0000",
201 to "5c5c5c7b7b7bb8b8b8d7d7d78a0f0fb81414ec4848f176760f8a0f14b81448ec4876f1760f0f8a1414b84848ec7676f18a8a0fb8b814ecec48f1f1768a0f8ab814b8ec48ecf176f10f8a8a14b8b848ecec76f1f1",
390 to "ffffcdffff9acdcd9affff66cdcd669a9a66ffff33cdcd339a9a33666633ffff00cdcd009a9a00666600333300",
406 to "cdffcd9aff9a9acd9a66ff6666cd66669a6633ff3333cd33339a3333663300ff0000cd00009a00006600003300",
422 to "cdffff9affff9acdcd66ffff66cdcd669a9a33ffff33cdcd339a9a33666600ffff00cdcd009a9a006666003333",
590 to "cdcdff9a9aff9a9acd6666ff6666cd66669a3333ff3333cd33339a3333660000ff0000cd00009a000066000033",
606 to "ffcdffff9affcd9acdff66ffcd66cd9a669aff33ffcd33cd9a339a663366ff00ffcd00cd9a009a660066330033",
622 to "ffcdcdff9a9acd9a9aff6666cd66669a6666ff3333cd33339a3333663333ff0000cd00009a0000660000330000",
791 to "ffcd9acd9a669a66339a6600cd9a33ffcd66ff9a00ffcd33cd9a00ffcd00ff9a33cd66006633009a3300cd6633ff9a66ff6600ff6633cd3300ff33009aff3366cd00336600339a0066cd339aff6666ff0066ff3333cd0033ff00cdff9a9acd66669a33669a009acd33cdff669aff00cdff339acd00cdff009affcd66cd9a339a66009a6633cd9a66ffcd00ff6633ffcd00cd9a00ffcd33ff9a00cd66006633009a3333cd6666ff9a00ff9a33ff6600cd3300ff339acdff669acd33669a00339a3366cd669aff0066ff3366ff0033cd0033ff339aff0066cd00336600669a339acd66cdff009aff33cdff009acd00cdffcd9aff9a66cd66339a66009a9a33cdcd66ff9a00ffcd33ff9a00cdcd00ff9a33ff6600cd33006633009a6633cd9a66ff6600ff6633ff3300cd3300ffff339acd00666600339a0033cd3366ff669aff0066ff3366cd0033ff0033ff9acdcd669a9a33669a0066cd339aff66cdff009acd009aff33cdff009a",
920 to "cdcdcd9a9a9a666666333333"
)
init {
colorMap[0] = "white"
colorMap[1] = "black"
colorMap[2] = "red"
colorMap[3] = "green"
colorMap[4] = "blue"
colorMap[5] = "yellow"
colorMap[6] = "magenta"
colorMap[7] = "cyan"
colorMap[8] = "rgb(89,212,84)"
colorMap[9] = "rgb(89,84,217)"
colorMap[10] = "white"
moreCol.forEach { (n, s) ->
for (i in 0 until (s.length / 6)) {
colorMap[n + i] = "#" + s.substring(i * 6, (i + 1) * 6)
}
}
}
public operator fun get(index: Int): String = colorMap[index]
}

View File

@ -0,0 +1,17 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("TGeoManager")
public class TGeoManager : TNamed() {
public val fMatrices: TObjArray<TGeoMatrix> = TObjArray.getEmpty()
public val fShapes: TObjArray<TGeoShape> = TObjArray.getEmpty()
public val fVolumes: TObjArray<TGeoVolume> = TObjArray.getEmpty()
public val fNodes: TObjArray<TGeoNode> = TObjArray.getEmpty()
}

View File

@ -0,0 +1,12 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("TGeoMaterial")
public open class TGeoMaterial: TNamed()
@Serializable
@SerialName("TGeoMixture")
public class TGeoMixture: TGeoMaterial()

View File

@ -0,0 +1,42 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("TGeoMatrix")
public sealed class TGeoMatrix : TNamed()
@Serializable
@SerialName("TGeoIdentity")
public class TGeoIdentity : TGeoMatrix()
@Serializable
@SerialName("TGeoHMatrix")
public class TGeoHMatrix(
public val fTranslation: DoubleArray,
public val fRotationMatrix: DoubleArray,
public val fScale: DoubleArray
) : TGeoMatrix()
@Serializable
@SerialName("TGeoTranslation")
public class TGeoTranslation(
public val fTranslation: DoubleArray
) : TGeoMatrix()
@Serializable
@SerialName("TGeoRotation")
public class TGeoRotation(
public val fRotationMatrix: DoubleArray
) : TGeoMatrix()
@Serializable
@SerialName("TGeoCombiTrans")
public class TGeoCombiTrans(
public val fTranslation: DoubleArray,
@Contextual
public val fRotation: TGeoRotation? = null,
) : TGeoMatrix()

View File

@ -0,0 +1,14 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("TGeoMedium")
public class TGeoMedium(
public val fId: Int,
@Contextual
public val fMaterial: TGeoMaterial,
public val fParams: DoubleArray
) : TNamed()

View File

@ -0,0 +1,34 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("TGeoNode")
public open class TGeoNode : TNamed() {
public val fGeoAtt: UInt = 0u
@Contextual
public val fVolume: TGeoVolume? = null
// @Contextual
// public val fMother: TGeoVolume? = null
public val fNumber: Int = 0
public val fNovlp: Int = 0
public val fOverlaps: IntArray = intArrayOf()
}
@Serializable
@SerialName("TGeoNodeMatrix")
public class TGeoNodeMatrix : TGeoNode() {
@Contextual
public val fMatrix: TGeoMatrix? = null
}
@Serializable
@SerialName("TGeoNodeOffset")
public class TGeoNodeOffset : TGeoNode() {
public val fOffset: Double = 0.0
}

View File

@ -0,0 +1,136 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.math.PI
@Serializable
@SerialName("TGeoShape")
public sealed class TGeoShape : TNamed() {
public val fShapeBits: UInt = 0u
public val fShapeId: Int = 0
}
@Serializable
@SerialName("TGeoBBox")
public open class TGeoBBox : TGeoShape() {
public val fDX: Double = 0.0
public val fDY: Double = 0.0
public val fDZ: Double = 0.0
public val fOrigin: DoubleArray = doubleArrayOf(0.0, 0.0, 0.0)
}
@Serializable
@SerialName("TGeoBoolNode")
public sealed class TGeoBoolNode : TObject() {
@Contextual
public abstract val fLeft: TGeoShape
@Contextual
public val fLeftMat: TGeoMatrix? = null
@Contextual
public abstract val fRight: TGeoShape
@Contextual
public val fRightMat: TGeoMatrix? = null
}
@Serializable
@SerialName("TGeoUnion")
public class TGeoUnion(
@Contextual
override val fLeft: TGeoShape,
@Contextual
override val fRight: TGeoShape,
) : TGeoBoolNode()
@Serializable
@SerialName("TGeoSubtraction")
public class TGeoSubtraction(
@Contextual
override val fLeft: TGeoShape,
@Contextual
override val fRight: TGeoShape,
) : TGeoBoolNode()
@Serializable
@SerialName("TGeoIntersection")
public class TGeoIntersection(
@Contextual
override val fLeft: TGeoShape,
@Contextual
override val fRight: TGeoShape,
) : TGeoBoolNode()
@Serializable
@SerialName("TGeoCompositeShape")
public class TGeoCompositeShape(public val fNode: TGeoBoolNode) : TGeoBBox()
@Serializable
@SerialName("TGeoXtru")
public class TGeoXtru(
public val fNvert: Int,
public val fNz: Int,
public val fZcurrent: Double,
public val fX: DoubleArray,
public val fY: DoubleArray,
public val fZ: DoubleArray,
public val fScale: DoubleArray,
public val fX0: DoubleArray,
public val fY0: DoubleArray
) : TGeoBBox()
@Serializable
@SerialName("TGeoTube")
public open class TGeoTube : TGeoBBox() {
public val fRmin: Double = 0.0
public val fRmax: Double = 0.0
public val fDz: Double = 0.0
}
@Serializable
@SerialName("TGeoTubeSeg")
public class TGeoTubeSeg(
public val fPhi1: Double,
public val fPhi2: Double,
public val fS1: Double,
public val fC1: Double,
public val fS2: Double,
public val fC2: Double,
public val fSm: Double,
public val fCm: Double,
public val fCdfi: Double,
) : TGeoTube()
@Serializable
@SerialName("TGeoPcon")
public open class TGeoPcon : TGeoBBox() {
public val fNz: Int = 0 // number of z planes (at least two)
public val fPhi1: Double = 0.0 // lower phi limit (converted to [0,2*pi)
public val fDphi: Double = PI * 2 // phi range
public val fRmin: DoubleArray = doubleArrayOf() //[fNz] pointer to array of inner radii
public val fRmax: DoubleArray = doubleArrayOf() //[fNz] pointer to array of outer radii
public val fZ: DoubleArray = doubleArrayOf() //[fNz] pointer to array of Z planes positions
}
@Serializable
@SerialName("TGeoPgon")
public open class TGeoPgon : TGeoPcon() {
public val fNedges: Int = 0
}
@Serializable
@SerialName("TGeoShapeAssembly")
public class TGeoShapeAssembly(
@Contextual
public val fVolume: TGeoVolumeAssembly,
public val fBBoxOK: Boolean = true
) : TGeoBBox()
public class TGeoShapeRef(provider: () -> TGeoShape) : TGeoShape() {
public val value: TGeoShape by lazy(provider)
}

View File

@ -0,0 +1,41 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("TGeoVolume")
public open class TGeoVolume : TNamed() {
public val fGeoAtt: UInt = 0u
public val fLineColor: Int = 2
public val fLineStyle: Int? = null
public val fLineWidth: UInt = 1u
public val fFillColor: Int? = null
public val fFillStyle: Int? = null
@Contextual
public val fNodes: TObjArray<@Contextual TGeoNode>? = null
@Contextual
public val fShape: TGeoShape? = null
@Contextual
public val fMedium: TGeoMedium? = null
public val fNumber: Int = 1
public val fNtotal: Int = 1
public val fRefCount: Int = 1
}
public class TGeoVolumeRef(provider: () -> TGeoVolume) : TGeoVolume() {
public val value: TGeoVolume by lazy(provider)
}
@Serializable
@SerialName("TGeoVolumeAssembly")
public open class TGeoVolumeAssembly : TGeoVolume()
public class TGeoVolumeAssemblyRef(provider: () -> TGeoVolumeAssembly) : TGeoVolumeAssembly() {
public val value: TGeoVolumeAssembly by lazy(provider)
}

View File

@ -0,0 +1,34 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
public abstract class TObject {
public val fUniqueID: UInt = 0u
public val fBits: UInt = 0u
}
@Serializable
public open class TNamed : TObject() {
public val fName: String = ""
public val fTitle: String = ""
}
@Serializable
@SerialName("TObjArray")
public class TObjArray<T: TObject>(public val arr: List<@Contextual T>): TObject() {
public companion object{
public fun <T: TObject> getEmpty(): TObjArray<T> = TObjArray(emptyList())
}
}
@Serializable
@SerialName("TList")
public class TList(public val arr: List<@Contextual TObject>): TObject()
@Serializable
@SerialName("THashList")
public class THashList(public val arr: List<@Contextual TObject>): TObject()

View File

@ -0,0 +1,235 @@
package ru.mipt.npm.root.serialization
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
private fun <T> jsonRootDeserializer(
tSerializer: KSerializer<T>,
builder: (JsonElement) -> T
): DeserializationStrategy<T> = object :
DeserializationStrategy<T> {
private val jsonElementSerializer = JsonElement.serializer()
override val descriptor: SerialDescriptor
get() = jsonElementSerializer.descriptor
override fun deserialize(decoder: Decoder): T {
val json = decoder.decodeSerializableValue(jsonElementSerializer)
return builder(json)
}
}
/**
* Load Json encoded TObject
*/
public fun <T : TObject> TObject.decodeFromJson(serializer: KSerializer<T>, jsonElement: JsonElement): T =
RootDecoder.decode(serializer, jsonElement)
public fun <T : TObject> TObject.decodeFromString(serializer: KSerializer<T>, string: String): T {
val json = Json.parseToJsonElement(string)
return RootDecoder.decode(serializer, json)
}
private object RootDecoder {
private class RootUnrefSerializer<T>(
private val tSerializer: KSerializer<T>,
private val refCache: List<RefEntry>,
) : KSerializer<T> by tSerializer {
override fun deserialize(decoder: Decoder): T {
val input = decoder as JsonDecoder
val element = input.decodeJsonElement()
val refId = (element as? JsonObject)?.get("\$ref")?.jsonPrimitive?.int
val ref = if (refId != null) {
println("Substituting ${tSerializer.descriptor.serialName} ref $refId")
//Forward ref for shapes
when (tSerializer.descriptor.serialName) {
"TGeoShape" -> return TGeoShapeRef {
refCache[refId].getOrPutValue {
input.json.decodeFromJsonElement(tSerializer, it) as TGeoShape
}
} as T
"TGeoVolumeAssembly" -> return TGeoVolumeAssemblyRef {
refCache[refId].getOrPutValue {
input.json.decodeFromJsonElement(tSerializer, it) as TGeoVolumeAssembly
}
} as T
"TGeoVolume" -> return TGeoVolumeRef {
refCache[refId].getOrPutValue {
input.json.decodeFromJsonElement(tSerializer, it) as TGeoVolume
}
} as T
//Do unref
else -> refCache[refId]
}
} else {
refCache.find { it.element == element } ?: error("Element '$element' not found in the cache")
}
return ref.getOrPutValue {
// println("Decoding $it")
val actualTypeName = it.jsonObject["_typename"]?.jsonPrimitive?.content
input.json.decodeFromJsonElement(tSerializer, it)
}
}
}
private fun <T> KSerializer<T>.unref(refCache: List<RefEntry>): KSerializer<T> = RootUnrefSerializer(this, refCache)
@OptIn(ExperimentalSerializationApi::class)
fun unrefSerializersModule(
refCache: List<RefEntry>
): SerializersModule = SerializersModule {
contextual(TObjArray::class) {
TObjArray.serializer(it[0]).unref(refCache)
}
contextual(TGeoMedium.serializer().unref(refCache))
polymorphic(TGeoBoolNode::class) {
subclass(TGeoIntersection.serializer().unref(refCache))
subclass(TGeoUnion.serializer().unref(refCache))
subclass(TGeoSubtraction.serializer().unref(refCache))
}
polymorphic(TGeoShape::class) {
subclass(TGeoBBox.serializer())
subclass(TGeoXtru.serializer())
subclass(TGeoTube.serializer())
subclass(TGeoTubeSeg.serializer())
subclass(TGeoPcon.serializer())
subclass(TGeoPgon.serializer())
subclass(TGeoCompositeShape.serializer().unref(refCache))
subclass(TGeoShapeAssembly.serializer().unref(refCache))
default {
if (it == null) {
TGeoShape.serializer().unref(refCache)
} else {
error("Unrecognized shape $it")
}
}
}
polymorphic(TGeoMatrix::class) {
subclass(TGeoIdentity.serializer())
subclass(TGeoHMatrix.serializer().unref(refCache))
subclass(TGeoTranslation.serializer())
subclass(TGeoRotation.serializer())
subclass(TGeoCombiTrans.serializer().unref(refCache))
val unrefed = TGeoMatrix.serializer().unref(refCache)
default {
if (it == null) {
unrefed
} else {
error("Unrecognized matrix $it")
}
}
}
polymorphic(TGeoVolume::class, TGeoVolume.serializer().unref(refCache)) {
subclass(TGeoVolumeAssembly.serializer().unref(refCache))
val unrefed = TGeoVolume.serializer().unref(refCache)
default {
if (it == null) {
unrefed
} else {
error("Unrecognized volume $it")
}
}
}
polymorphic(TGeoNode::class, TGeoNode.serializer().unref(refCache)) {
subclass(TGeoNodeMatrix.serializer().unref(refCache))
subclass(TGeoNodeOffset.serializer().unref(refCache))
val unrefed = TGeoNode.serializer().unref(refCache)
default {
if (it == null) {
unrefed
} else {
error("Unrecognized node $it")
}
}
}
}
/**
* Create an instance of Json with unfolding Root references. This instance could not be reused because of the cache.
*/
private fun unrefJson(refCache: MutableList<RefEntry>): Json = Json {
encodeDefaults = true
ignoreUnknownKeys = true
classDiscriminator = "_typename"
serializersModule = unrefSerializersModule(refCache)
}
fun <T : TObject> decode(sourceDeserializer: KSerializer<T>, source: JsonElement): T {
val refCache = ArrayList<RefEntry>()
fun fillCache(element: JsonElement) {
when (element) {
is JsonObject -> {
if (element["_typename"] != null) {
refCache.add(RefEntry(element))
}
element.values.forEach {
fillCache(it)
}
}
is JsonArray -> {
element.forEach {
fillCache(it)
}
}
else -> {
//ignore primitives
}
}
}
fillCache(source)
return unrefJson(refCache).decodeFromJsonElement(sourceDeserializer.unref(refCache), source)
}
class RefEntry(val element: JsonElement) {
var value: Any? = null
fun <T> getOrPutValue(builder: (JsonElement) -> T): T {
if (value == null) {
value = builder(element)
}
return value as T
}
override fun toString(): String = element.toString()
}
// val json = Json {
// encodeDefaults = true
// ignoreUnknownKeys = true
// classDiscriminator = "_typename"
// serializersModule = this@RootDecoder.serializersModule
// }
}

View File

@ -0,0 +1,187 @@
package ru.mipt.npm.root.serialization
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.plus
import space.kscience.visionforge.solid.*
import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.pow
import kotlin.math.sqrt
private val solidsName = "solids".asName()
private val volumesName = "volumes".asName()
private operator fun Number.times(d: Double) = toDouble() * d
private operator fun Number.times(f: Float) = toFloat() * f
private fun degToRad(d: Double) = d * PI / 180.0
// converting to XYZ to TaitBryan angles according to https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix
private fun Solid.rotate(rot: DoubleArray) {
val xAngle = atan2(-rot[5], rot[8])
val yAngle = atan2(rot[2], sqrt(1.0 - rot[2].pow(2)))
val zAngle = atan2(-rot[1], rot[0])
rotation = Point3D(xAngle, yAngle, zAngle)
}
private fun Solid.translate(trans: DoubleArray) {
val (x, y, z) = trans
position = Point3D(x, y, z)
}
private fun Solid.useMatrix(matrix: TGeoMatrix?) {
when (matrix) {
null, is TGeoIdentity -> {
//do nothing
}
is TGeoTranslation -> {
translate(matrix.fTranslation)
}
is TGeoRotation -> {
rotate(matrix.fRotationMatrix)
}
is TGeoCombiTrans -> {
translate(matrix.fTranslation)
matrix.fRotation?.let { rotate(it.fRotationMatrix) }
}
is TGeoHMatrix -> {
translate(matrix.fTranslation)
rotate(matrix.fRotationMatrix)
val (xScale, yScale, zScale) = matrix.fScale
scale = Point3D(xScale, yScale, zScale)
}
}
}
private fun SolidGroup.addShape(shape: TGeoShape) {
when (shape) {
is TGeoShapeRef -> addShape(shape.value)
is TGeoCompositeShape -> {
val bool: TGeoBoolNode = shape.fNode
val compositeType = when (bool) {
is TGeoIntersection -> CompositeType.INTERSECT
is TGeoSubtraction -> CompositeType.SUBTRACT
is TGeoUnion -> CompositeType.UNION
}
composite(compositeType, name = shape.fName) {
addShape(bool.fLeft).apply {
useMatrix(bool.fLeftMat)
}
addShape(bool.fRight).apply {
useMatrix(bool.fRightMat)
}
}
}
is TGeoXtru -> extruded(name = shape.fName) {
(0 until shape.fNvert).forEach { index ->
shape {
point(shape.fX[index], shape.fY[index])
}
}
(0 until shape.fNz).forEach { index ->
layer(
shape.fZ[index],
shape.fX0[index],
shape.fY0[index],
shape.fScale[index]
)
}
}
is TGeoTube -> tube(
radius = shape.fRmax,
height = shape.fDz * 2,
innerRadius = shape.fRmin,
name = shape.fName
)
is TGeoTubeSeg -> tube(
radius = shape.fRmax,
height = shape.fDz * 2,
innerRadius = shape.fRmin,
startAngle = degToRad(shape.fPhi1),
angle = degToRad(shape.fPhi2 - shape.fPhi1),
name = shape.fName
)
is TGeoPcon -> TODO()
is TGeoPgon -> TODO()
is TGeoShapeAssembly -> volume(shape.fVolume)
is TGeoBBox -> box(shape.fDX * 2, shape.fDY * 2, shape.fDZ * 2, name = shape.fName)
}
}
private fun SolidGroup.node(obj: TGeoNode) {
if (obj.fVolume != null) {
volume(obj.fVolume, obj.fName).apply {
when (obj) {
is TGeoNodeMatrix -> {
useMatrix(obj.fMatrix)
}
is TGeoNodeOffset -> {
x = obj.fOffset
}
}
}
}
}
private fun buildGroup(volume: TGeoVolume): SolidGroup {
return if (volume is TGeoVolumeAssemblyRef) {
buildGroup(volume.value)
} else {
SolidGroup {
volume.fShape?.let { addShape(it) }
volume.fNodes?.let {
it.arr.forEach { obj ->
node(obj)
}
}
}
}
}
private val SolidGroup.rootPrototypes: SolidGroup get() = (parent as? SolidGroup)?.rootPrototypes ?: this
private fun SolidGroup.volume(volume: TGeoVolume, name: String? = null, cache: Boolean = true): Solid {
val group = buildGroup(volume)
val combinedName = if (volume.fName.isEmpty()) {
name
} else if (name == null) {
volume.fName
} else {
"${name}_${volume.fName}"
}
return if (!cache) {
group
} else newRef(
name = combinedName,
obj = group,
prototypeHolder = rootPrototypes,
templateName = volumesName + Name.parse(combinedName ?: "volume[${group.hashCode()}]")
)
}
// private fun load(geo: TGeoManager): SolidGroup {
//// /**
//// * A special group for local templates
//// */
//// val proto = SolidGroup()
////
//// val solids = proto.group(solidsName) {
//// setPropertyNode("edges.enabled", false)
//// }
////
//// val volumes = proto.group(volumesName)
////
//// val referenceStore = HashMap<Name, MutableList<SolidReferenceGroup>>()
// }
public fun TGeoManager.toSolid(): SolidGroup = SolidGroup {
fNodes.arr.forEach {
node(it)
}
}

View File

@ -21,7 +21,6 @@ class GDMLDemoApp : App(GDMLView::class)
class GDMLView : View() {
private val context = Context {
plugin(FX3DPlugin)
plugin(VisionManager)
}
private val fx3d = context.fetch(FX3DPlugin)

View File

@ -6,6 +6,7 @@ import react.RProps
import react.child
import react.functionalComponent
import space.kscience.dataforge.context.Context
import space.kscience.plotly.layout
import space.kscience.plotly.models.Trace
import space.kscience.visionforge.markup.VisionOfMarkup
import space.kscience.visionforge.react.flexRow
@ -91,7 +92,10 @@ val GravityDemo = functionalComponent<DemoProps> { props ->
height = 50.vh - 50.pt
}
plotly {
traces(velocityTrace)
traces(velocityTrace,energyTrace)
layout {
xaxis.title = "time"
}
}
Markup {
attrs {

View File

@ -32,7 +32,7 @@ val Markup = functionalComponent<MarkupProps>("Markup") { props ->
//TODO add new formats via plugins
else -> error("Format ${vision.format} not recognized")
}
vision.useProperty(VisionOfMarkup::content) { content ->
vision.useProperty(VisionOfMarkup::content) { content: String? ->
element.clear()
element.append {
markdown(flavour) { content ?: "" }

View File

@ -6,7 +6,6 @@ import kotlinx.html.script
import kotlinx.html.stream.createHTML
import kotlinx.html.unsafe
import org.jetbrains.kotlinx.jupyter.api.HTML
import org.jetbrains.kotlinx.jupyter.api.annotations.JupyterLibrary
import org.jetbrains.kotlinx.jupyter.api.libraries.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.misc.DFExperimental
@ -22,7 +21,6 @@ import space.kscience.visionforge.plotly.asVision
import space.kscience.visionforge.solid.Solids
import space.kscience.visionforge.visionManager
@JupyterLibrary
@DFExperimental
public class VisionForgePlayGroundForJupyter : JupyterIntegration() {

View File

@ -65,12 +65,6 @@ application {
mainClass.set("ru.mipt.npm.muon.monitor.server.MMServerKt")
}
tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile>() {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + "-Xir-property-lazy-initialization"
}
}
//distributions {
// main {
// contents {

View File

@ -55,6 +55,7 @@ kotlin {
api(project(":visionforge-gdml"))
api(project(":visionforge-plotly"))
api(projects.visionforge.visionforgeMarkdown)
api(projects.visionforge.cernRootLoader)
}
}

View File

@ -0,0 +1,109 @@
package space.kscience.visionforge.examples
import ru.mipt.npm.root.DGeoManager
import ru.mipt.npm.root.serialization.TGeoManager
import ru.mipt.npm.root.toSolid
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.isLeaf
import space.kscience.dataforge.values.string
import space.kscience.visionforge.solid.Solids
import java.nio.file.Paths
import kotlin.io.path.writeText
private fun Meta.countTypes(): Sequence<String> = sequence {
if (!isLeaf) {
get("_typename")?.value?.let { yield(it.string) }
items.forEach { yieldAll(it.value.countTypes()) }
}
}
fun main() {
val context = Context {
plugin(Solids)
}
val string = TGeoManager::class.java.getResourceAsStream("/root/BM@N.root.json")!!
.readAllBytes().decodeToString()
val geo = DGeoManager.parse(string)
val sizes = geo.meta.countTypes().groupBy { it }.mapValues { it.value.size }
sizes.forEach {
println(it)
}
val solid = geo.toSolid()
Paths.get("BM@N.vf.json").writeText(Solids.encodeToString(solid))
//println(Solids.encodeToString(solid))
context.makeVisionFile {
vision("canvas") {
solid
}
}
}
/* SolidGroup {
set(
"Coil",
solid.getPrototype("Coil".asName())!!.apply {
parent = null
}
)
*//* group("Shade") {
y = 200
color("red")
coneSurface(
bottomOuterRadius = 135,
bottomInnerRadius = 25,
height = 50,
topOuterRadius = 135,
topInnerRadius = 25,
angle = 1.5707964
) {
position = Point3D(79.6, 0, -122.1)
rotation = Point3D(-1.5707964, 0, 0)
}
coneSurface(
bottomOuterRadius = 135,
bottomInnerRadius = 25,
height = 50,
topOuterRadius = 135,
topInnerRadius = 25,
angle = 1.5707964
) {
position = Point3D(-79.6, 0, -122.1)
rotation = Point3D(1.5707964, 0, -3.1415927)
}
coneSurface(
bottomOuterRadius = 135,
bottomInnerRadius = 25,
height = 50,
topOuterRadius = 135,
topInnerRadius = 25,
angle = 1.5707964
) {
position = Point3D(79.6, 0, 122.1)
rotation = Point3D(1.5707964, 0, 0)
}
coneSurface(
bottomOuterRadius = 135,
bottomInnerRadius = 25,
height = 50,
topOuterRadius = 135,
topInnerRadius = 25,
angle = 1.5707964
) {
position = Point3D(-79.6, 0, 122.1)
rotation = Point3D(-1.5707964, 0, -3.1415927)
}
}*//*
}*/

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,7 @@
kotlin.code.style=official
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.mpp.stability.nowarn=true
kotlin.native.enableDependencyPropagation=false
kapt.use.worker.api=false
kapt.incremental.apt=false
kotlin.jupyter.add.scanner=false
org.gradle.jvmargs=-XX:MaxMetaspaceSize=1G
org.gradle.parallel=true

View File

@ -57,3 +57,7 @@ kscience {
readme {
maturity = ru.mipt.npm.gradle.Maturity.EXPERIMENTAL
}
tasks.named<org.jetbrains.kotlinx.jupyter.api.plugin.tasks.JupyterApiResourcesTask>("processJupyterApiResources") {
libraryProducers = listOf("space.kscience.visionforge.gdml.jupyter.GdmlForJupyter")
}

View File

@ -2,7 +2,6 @@ package space.kscience.visionforge.gdml.jupyter
import kotlinx.html.stream.createHTML
import org.jetbrains.kotlinx.jupyter.api.HTML
import org.jetbrains.kotlinx.jupyter.api.annotations.JupyterLibrary
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
import space.kscience.dataforge.context.Context
@ -16,7 +15,6 @@ import space.kscience.visionforge.html.embedAndRenderVisionFragment
import space.kscience.visionforge.solid.Solids
import space.kscience.visionforge.visionManager
@JupyterLibrary
@DFExperimental
internal class GdmlForJupyter : JupyterIntegration() {

View File

@ -32,6 +32,7 @@ include(
":visionforge-threejs",
":visionforge-threejs:visionforge-threejs-server",
":visionforge-gdml",
":cern-root-loader",
":visionforge-server",
":visionforge-plotly",
":visionforge-markdown",

View File

@ -83,7 +83,7 @@ private fun RBuilder.visionTree(props: ObjectTreeProps): Unit {
}
obj.children.entries
.filter { !it.key.toString().startsWith("@") } // ignore statics and other hidden children
.sortedBy { (it.value as? VisionGroup)?.isEmpty ?: true } // ignore empty groups
.sortedBy { (it.value as? VisionGroup)?.isEmpty() ?: true } // ignore empty groups
.forEach { (childToken, child) ->
styledDiv {
css {

View File

@ -13,9 +13,7 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.length
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionGroup
import space.kscience.visionforge.computeProperties
import space.kscience.visionforge.*
import space.kscience.visionforge.react.ThreeCanvasComponent
import space.kscience.visionforge.react.flexColumn
import space.kscience.visionforge.react.flexRow
@ -85,7 +83,9 @@ public val ThreeCanvasWithControls: FunctionComponent<ThreeCanvasWithControlsPro
useEffect {
props.context.launch {
solid = props.builderOfSolid.await()
solid = props.builderOfSolid.await().also {
it?.root(props.context.visionManager)
}
}
}

View File

@ -63,7 +63,7 @@ public interface VisionGroup : Provider, Vision, VisionContainer<Vision> {
*/
public operator fun VisionGroup.iterator(): Iterator<Vision> = children.values.iterator()
public val VisionGroup.isEmpty: Boolean get() = this.children.isEmpty()
public fun VisionGroup.isEmpty(): Boolean = this.children.isEmpty()
public interface VisionContainerBuilder<in V : Vision> {
//TODO add documentation

View File

@ -76,7 +76,7 @@ public open class VisionGroupBase(
* Set parent for given child and attach it
*/
private fun attachChild(token: NameToken, child: Vision?) {
val before = children[token]
val before = childrenInternal[token]
when {
child == null -> {
childrenInternal.remove(token)

View File

@ -67,8 +67,8 @@ internal fun checkOrStoreFile(htmlPath: Path, filePath: Path, resource: String):
if (!skip) {
logger.debug("File $fullPath does not exist or wrong checksum. Writing file")
Files.createDirectories(fullPath.parent)
Files.write(fullPath, bytes, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
Files.write(md5File, checksum.encodeToByteArray(), StandardOpenOption.CREATE, StandardOpenOption.WRITE)
Files.write(fullPath, bytes, StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)
Files.write(md5File, checksum.encodeToByteArray(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)
}
return if (htmlPath.isAbsolute && fullPath.startsWith(htmlPath.parent)) {

View File

@ -27,6 +27,8 @@ import kotlin.reflect.KClass
public class FX3DPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
public val solids: Solids by require(Solids)
private val objectFactories = HashMap<KClass<out Solid>, FX3DFactory<*>>()
private val compositeFactory = FXCompositeFactory(this)
private val referenceFactory = FXReferenceFactory(this)
@ -50,6 +52,7 @@ public class FX3DPlugin : AbstractPlugin() {
is SolidGroup -> {
Group(obj.children.mapNotNull { (token, obj) ->
(obj as? Solid)?.let {
logger.info { token.toString() }
buildNode(it).apply {
properties["name"] = token.toString()
}

View File

@ -48,7 +48,7 @@ public class FXCompositeFactory(public val plugin: FX3DPlugin) : FX3DFactory<Com
val firstCSG = first.toCSG()
val secondCSG = second.toCSG()
val resultCSG = when (obj.compositeType) {
CompositeType.SUM, CompositeType.UNION -> firstCSG.union(secondCSG)
CompositeType.GROUP, CompositeType.UNION -> firstCSG.union(secondCSG)
CompositeType.INTERSECT -> firstCSG.intersect(secondCSG)
CompositeType.SUBTRACT -> firstCSG.difference(secondCSG)
}

View File

@ -1,5 +1,4 @@
plugins {
kotlin("multiplatform")
id("ru.mipt.npm.gradle.mpp")
}

View File

@ -0,0 +1,95 @@
package space.kscience.visionforge.gdml
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.names.Name
import space.kscience.gdml.*
import space.kscience.visionforge.solid.Solid
import space.kscience.visionforge.solid.SolidMaterial
import space.kscience.visionforge.solid.invoke
import space.kscience.visionforge.useStyle
import kotlin.random.Random
public class GdmlLoaderOptions {
public enum class Action {
ADD,
REJECT,
PROTOTYPE
}
public var lUnit: LUnit = LUnit.MM
public var aUnit: AUnit = AUnit.RADIAN
public var solidAction: (GdmlSolid) -> Action = { Action.PROTOTYPE }
public var volumeAction: (GdmlGroup) -> Action = { Action.PROTOTYPE }
internal val styleCache = HashMap<Name, Meta>()
public fun Solid.registerAndUseStyle(name: String, builder: MutableMeta.() -> Unit) {
styleCache.getOrPut(Name.parse(name)) {
Meta(builder)
}
useStyle(name)
}
public fun Solid.transparent() {
registerAndUseStyle("transparent") {
SolidMaterial.MATERIAL_OPACITY_KEY put 0.3
"edges.enabled" put true
}
}
/**
* Configure paint for given solid with given [GdmlMaterial]
*/
public var configurePaint: SolidMaterial.(material: GdmlMaterial, solid: GdmlSolid) -> Unit =
{ material, _ -> color(randomColor(material)) }
private set
public fun paint(block: SolidMaterial.(material: GdmlMaterial, solid: GdmlSolid) -> Unit) {
configurePaint = block
}
/**
* Configure given solid
*/
public var configureSolid: Solid.(parent: GdmlVolume, solid: GdmlSolid, material: GdmlMaterial) -> Unit =
{ parent, solid, material ->
val styleName = "materials.${material.name}"
if (parent.physVolumes.isNotEmpty()) transparent()
registerAndUseStyle(styleName) {
val vfMaterial = SolidMaterial().apply {
configurePaint(material, solid)
}
SolidMaterial.MATERIAL_KEY put vfMaterial.toMeta()
"Gdml.material" put material.name
}
}
private set
public fun configure(block: Solid.(parent: GdmlVolume, solid: GdmlSolid, material: GdmlMaterial) -> Unit) {
val oldConfigure = configureSolid
configureSolid = { parent: GdmlVolume, solid: GdmlSolid, material: GdmlMaterial ->
oldConfigure(parent, solid, material)
block(parent, solid, material)
}
}
public companion object {
private val random: Random = Random(222)
private val colorCache = HashMap<GdmlMaterial, Int>()
/**
* Use random color and cache it based on the material. Meaning that colors are random, but always the same for the
* same material.
*/
public fun randomColor(material: GdmlMaterial): Int {
return colorCache.getOrPut(material) { random.nextInt(16777216) }
}
}
}

View File

@ -1,7 +1,5 @@
package space.kscience.visionforge.gdml
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
@ -11,10 +9,8 @@ import space.kscience.gdml.*
import space.kscience.visionforge.*
import space.kscience.visionforge.html.VisionOutput
import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.SolidMaterial.Companion.MATERIAL_KEY
import kotlin.math.cos
import kotlin.math.sin
import kotlin.random.Random
private val solidsName = "solids".asName()
private val volumesName = "volumes".asName()
@ -25,91 +21,7 @@ private inline operator fun Number.times(d: Double) = toDouble() * d
@Suppress("NOTHING_TO_INLINE")
private inline operator fun Number.times(f: Float) = toFloat() * f
public class GdmlTransformer {
public enum class Action {
ADD,
REJECT,
PROTOTYPE
}
public var lUnit: LUnit = LUnit.MM
public var aUnit: AUnit = AUnit.RADIAN
public var solidAction: (GdmlSolid) -> Action = { Action.PROTOTYPE }
public var volumeAction: (GdmlGroup) -> Action = { Action.PROTOTYPE }
internal val styleCache = HashMap<Name, Meta>()
public fun Solid.registerAndUseStyle(name: String, builder: MutableMeta.() -> Unit) {
styleCache.getOrPut(Name.parse(name)) {
Meta(builder)
}
useStyle(name)
}
public fun Solid.transparent() {
registerAndUseStyle("transparent") {
SolidMaterial.MATERIAL_OPACITY_KEY put 0.3
"edges.enabled" put true
}
}
/**
* Configure paint for given solid with given [GdmlMaterial]
*/
public var configurePaint: SolidMaterial.(material: GdmlMaterial, solid: GdmlSolid) -> Unit =
{ material, _ -> color(randomColor(material)) }
private set
public fun paint(block: SolidMaterial.(material: GdmlMaterial, solid: GdmlSolid) -> Unit) {
configurePaint = block
}
/**
* Configure given solid
*/
public var configureSolid: Solid.(parent: GdmlVolume, solid: GdmlSolid, material: GdmlMaterial) -> Unit =
{ parent, solid, material ->
val styleName = "materials.${material.name}"
if (parent.physVolumes.isNotEmpty()) transparent()
registerAndUseStyle(styleName) {
val vfMaterial = SolidMaterial().apply {
configurePaint(material, solid)
}
MATERIAL_KEY put vfMaterial.toMeta()
"Gdml.material" put material.name
}
}
private set
public fun configure(block: Solid.(parent: GdmlVolume, solid: GdmlSolid, material: GdmlMaterial) -> Unit) {
val oldConfigure = configureSolid
configureSolid = { parent: GdmlVolume, solid: GdmlSolid, material: GdmlMaterial ->
oldConfigure(parent, solid, material)
block(parent, solid, material)
}
}
public companion object {
private val random: Random = Random(222)
private val colorCache = HashMap<GdmlMaterial, Int>()
/**
* Use random color and cache it based on the material. Meaning that colors are random, but always the same for the
* same material.
*/
public fun randomColor(material: GdmlMaterial): Int {
return colorCache.getOrPut(material) { random.nextInt(16777216) }
}
}
}
private class GdmlTransformerEnv(val settings: GdmlTransformer) {
private class GdmlLoader(val settings: GdmlLoaderOptions) {
//private val materialCache = HashMap<GdmlMaterial, Meta>()
/**
@ -303,12 +215,12 @@ private class GdmlTransformerEnv(val settings: GdmlTransformer) {
val first: GdmlSolid = solid.first.resolve(root) ?: error("")
val second: GdmlSolid = solid.second.resolve(root) ?: error("")
val type: CompositeType = when (solid) {
is GdmlUnion -> CompositeType.SUM // dumb sum for better performance
is GdmlUnion -> CompositeType.UNION // dumb sum for better performance
is GdmlSubtraction -> CompositeType.SUBTRACT
is GdmlIntersection -> CompositeType.INTERSECT
}
return composite(type, name) {
return smartComposite(type, name) {
addSolid(root, first).withPosition(
solid.resolveFirstPosition(root),
solid.resolveFirstRotation(root),
@ -356,13 +268,13 @@ private class GdmlTransformerEnv(val settings: GdmlTransformer) {
): Solid? {
require(name != "") { "Can't use empty solid name. Use null instead." }
return when (settings.solidAction(solid)) {
GdmlTransformer.Action.ADD -> {
GdmlLoaderOptions.Action.ADD -> {
addSolid(root, solid, name)
}
GdmlTransformer.Action.PROTOTYPE -> {
GdmlLoaderOptions.Action.PROTOTYPE -> {
proxySolid(root, this, solid, name ?: solid.name)
}
GdmlTransformer.Action.REJECT -> {
GdmlLoaderOptions.Action.REJECT -> {
//ignore
null
}
@ -388,14 +300,14 @@ private class GdmlTransformerEnv(val settings: GdmlTransformer) {
}
when (settings.volumeAction(volume)) {
GdmlTransformer.Action.ADD -> {
GdmlLoaderOptions.Action.ADD -> {
val group: SolidGroup = volume(root, volume)
this[physVolume.name] = group.withPosition(root, physVolume)
}
GdmlTransformer.Action.PROTOTYPE -> {
GdmlLoaderOptions.Action.PROTOTYPE -> {
proxyVolume(root, this, physVolume, volume)
}
GdmlTransformer.Action.REJECT -> {
GdmlLoaderOptions.Action.REJECT -> {
//ignore
}
}
@ -460,16 +372,16 @@ private class GdmlTransformerEnv(val settings: GdmlTransformer) {
}
public fun Gdml.toVision(block: GdmlTransformer.() -> Unit = {}): SolidGroup {
val settings = GdmlTransformer().apply(block)
val context = GdmlTransformerEnv(settings)
public fun Gdml.toVision(block: GdmlLoaderOptions.() -> Unit = {}): SolidGroup {
val settings = GdmlLoaderOptions().apply(block)
val context = GdmlLoader(settings)
return context.transform(this)
}
/**
* Append Gdml node to the group
*/
public fun SolidGroup.gdml(gdml: Gdml, key: String? = null, transformer: GdmlTransformer.() -> Unit = {}) {
public fun SolidGroup.gdml(gdml: Gdml, key: String? = null, transformer: GdmlLoaderOptions.() -> Unit = {}) {
val visual = gdml.toVision(transformer)
//println(Visual3DPlugin.json.stringify(VisualGroup3D.serializer(), visual))
set(key, visual)

View File

@ -9,7 +9,7 @@ public fun SolidGroup.gdml(
file: Path,
key: String = "",
usePreprocessor: Boolean = false,
transformer: GdmlTransformer.() -> Unit = {},
transformer: GdmlLoaderOptions.() -> Unit = {},
) {
val gdml = Gdml.decodeFromFile(file, usePreprocessor)
gdml(gdml, key, transformer)

View File

@ -2,13 +2,15 @@ package space.kscience.visionforge.solid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.isEmpty
import space.kscience.dataforge.meta.update
import space.kscience.visionforge.VisionBuilder
import space.kscience.visionforge.VisionContainerBuilder
import space.kscience.visionforge.VisionPropertyContainer
import space.kscience.visionforge.set
public enum class CompositeType {
SUM, // Dumb sum of meshes
GROUP, // Dumb sum of meshes
UNION, //CSG union
INTERSECT,
SUBTRACT
@ -20,7 +22,7 @@ public class Composite(
public val compositeType: CompositeType,
public val first: Solid,
public val second: Solid,
) : SolidBase(), Solid
) : SolidBase(), VisionPropertyContainer<Composite>
@VisionBuilder
public inline fun VisionContainerBuilder<Solid>.composite(
@ -30,7 +32,9 @@ public inline fun VisionContainerBuilder<Solid>.composite(
): Composite {
val group = SolidGroup().apply(builder)
val children = group.children.values.filterIsInstance<Solid>()
if (children.size != 2) error("Composite requires exactly two children")
if (children.size != 2){
error("Composite requires exactly two children, but found ${children.size}")
}
val res = Composite(type, children[0], children[1])
res.meta.update(group.meta)
@ -49,6 +53,31 @@ public inline fun VisionContainerBuilder<Solid>.composite(
return res
}
/**
* A smart form of [Composite] that in case of [CompositeType.GROUP] creates a static group instead
*/
@VisionBuilder
public fun SolidGroup.smartComposite(
type: CompositeType,
name: String? = null,
builder: SolidGroup.() -> Unit,
): Solid = if (type == CompositeType.GROUP) {
val group = SolidGroup(builder)
if (name == null && group.meta.isEmpty()) {
//append directly to group if no properties are defined
group.children.forEach { (key, value) ->
value.parent = null
set(null, value)
}
this
} else {
set(name, group)
group
}
} else {
composite(type, name, builder)
}
@VisionBuilder
public inline fun VisionContainerBuilder<Solid>.union(
name: String? = null,

View File

@ -4,6 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.visionforge.VisionBuilder
import space.kscience.visionforge.VisionContainerBuilder
import space.kscience.visionforge.VisionPropertyContainer
import space.kscience.visionforge.set
import kotlin.math.PI
import kotlin.math.cos
@ -23,7 +24,7 @@ public class ConeSurface(
public val topInnerRadius: Float,
public val startAngle: Float = 0f,
public val angle: Float = PI2,
) : SolidBase(), GeometrySolid {
) : SolidBase(), GeometrySolid, VisionPropertyContainer<ConeSurface> {
init {
require(bottomRadius > 0) { "Cone surface bottom radius must be positive" }

View File

@ -3,11 +3,12 @@ package space.kscience.visionforge.solid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.visionforge.VisionContainerBuilder
import space.kscience.visionforge.VisionPropertyContainer
import space.kscience.visionforge.set
@Serializable
@SerialName("solid.convex")
public class Convex(public val points: List<Point3D>) : SolidBase(), Solid
public class Convex(public val points: List<Point3D>) : SolidBase(), VisionPropertyContainer<Convex>
public inline fun VisionContainerBuilder<Solid>.convex(name: String? = null, action: ConvexBuilder.() -> Unit = {}): Convex =
ConvexBuilder().apply(action).build().also { set(name, it) }

View File

@ -57,8 +57,8 @@ public inline fun VisionContainerBuilder<Solid>.box(
ySize: Number,
zSize: Number,
name: String? = null,
action: Box.() -> Unit = {},
): Box = Box(xSize.toFloat(), ySize.toFloat(), zSize.toFloat()).apply(action).also { set(name, it) }
block: Box.() -> Unit = {},
): Box = Box(xSize.toFloat(), ySize.toFloat(), zSize.toFloat()).apply(block).also { set(name, it) }
@Serializable
@SerialName("solid.hexagon")

View File

@ -5,14 +5,11 @@ import kotlinx.serialization.Serializable
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.plus
import space.kscience.visionforge.VisionBuilder
import space.kscience.visionforge.VisionContainerBuilder
import space.kscience.visionforge.numberProperty
import space.kscience.visionforge.set
import space.kscience.visionforge.*
@Serializable
@SerialName("solid.line")
public class PolyLine(public val points: List<Point3D>) : SolidBase(), Solid {
public class PolyLine(public val points: List<Point3D>) : SolidBase(), VisionPropertyContainer<PolyLine> {
//var lineType by string()
public var thickness: Number by numberProperty(name = SolidMaterial.MATERIAL_KEY + THICKNESS_KEY) { 1.0 }

View File

@ -82,8 +82,8 @@ public fun SolidGroup(block: SolidGroup.() -> Unit): SolidGroup {
@VisionBuilder
public fun VisionContainerBuilder<Vision>.group(
name: Name? = null,
action: SolidGroup.() -> Unit = {},
): SolidGroup = SolidGroup().apply(action).also { set(name, it) }
builder: SolidGroup.() -> Unit = {},
): SolidGroup = SolidGroup().apply(builder).also { set(name, it) }
/**
* Define a group with given [name], attach it to this parent and return it.

View File

@ -4,6 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.visionforge.VisionBuilder
import space.kscience.visionforge.VisionContainerBuilder
import space.kscience.visionforge.VisionPropertyContainer
import space.kscience.visionforge.set
@Serializable
@ -12,7 +13,7 @@ public class SolidLabel(
public val text: String,
public val fontSize: Double,
public val fontFamily: String,
) : SolidBase(), Solid
) : SolidBase(), VisionPropertyContainer<SolidLabel>
@VisionBuilder
public fun VisionContainerBuilder<Solid>.label(

View File

@ -158,21 +158,28 @@ public fun SolidGroup.ref(
name: String? = null,
): SolidReferenceGroup = SolidReferenceGroup(templateName).also { set(name, it) }
/**
* Add new [SolidReferenceGroup] wrapping given object and automatically adding it to the prototypes
*/
public fun SolidGroup.ref(
name: String,
templateName: String,
name: String? = null,
): SolidReferenceGroup = ref(Name.parse(templateName), name)
/**
* Add new [SolidReferenceGroup] wrapping given object and automatically adding it to the prototypes.
* One must ensure that [prototypeHolder] is a parent of this group.
*/
public fun SolidGroup.newRef(
name: String?,
obj: Solid,
templateName: Name = Name.parse(name),
prototypeHolder: PrototypeHolder = this,
templateName: Name = Name.parse(name ?: obj.toString()),
): SolidReferenceGroup {
val existing = getPrototype(templateName)
if (existing == null) {
prototypes {
this[templateName] = obj
prototypeHolder.prototypes {
set(templateName, obj)
}
} else if (existing != obj) {
error("Can't add different prototype on top of existing one")
}
return this@ref.ref(templateName, name)
return ref(templateName, name)
}

View File

@ -18,6 +18,7 @@ import kotlin.reflect.KClass
public class Solids(meta: Meta) : VisionPlugin(meta) {
override val tag: PluginTag get() = Companion.tag
override val visionSerializersModule: SerializersModule get() = serializersModuleForSolids
public companion object : PluginFactory<Solids> {

View File

@ -5,7 +5,10 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaProvider
import space.kscience.dataforge.meta.float
import space.kscience.dataforge.meta.get
import space.kscience.visionforge.solid.Solid.Companion.X_KEY
import space.kscience.visionforge.solid.Solid.Companion.Y_KEY
import space.kscience.visionforge.solid.Solid.Companion.Z_KEY
@ -54,7 +57,7 @@ internal object Point3DSerializer : KSerializer<Point3D> {
override val descriptor: SerialDescriptor = Point3DImpl.serializer().descriptor
override fun deserialize(decoder: Decoder): Point3D = decoder.decodeSerializableValue(Point3DImpl.serializer())
override fun deserialize(decoder: Decoder): MutablePoint3D = decoder.decodeSerializableValue(Point3DImpl.serializer())
override fun serialize(encoder: Encoder, value: Point3D) {
val impl: Point3DImpl = (value as? Point3DImpl) ?: Point3DImpl(value.x, value.y, value.z)

View File

@ -16,7 +16,7 @@ fun SolidGroup.refGroup(
block: MutableVisionGroup.() -> Unit
): SolidReferenceGroup {
val group = SolidGroup().apply(block)
return ref(name, group, templateName)
return newRef(name, group, templateName = templateName)
}
@ -42,7 +42,7 @@ class SerializationTest {
z = -100
}
val group = SolidGroup {
ref("cube", cube)
newRef("cube", cube)
refGroup("pg", Name.parse("pg.content")) {
sphere(50) {
x = -100

View File

@ -14,7 +14,7 @@ class SolidReferenceTest {
val theStyle by style {
SolidMaterial.MATERIAL_COLOR_KEY put "red"
}
ref("test", Box(100f,100f,100f).apply {
newRef("test", Box(100f,100f,100f).apply {
color("blue")
useStyle(theStyle)
})

View File

@ -5,5 +5,5 @@ plugins {
dependencies {
api(project(":visionforge-solid"))
implementation(npm("three", "0.130.1"))
implementation(npm("three-csg-ts", "3.1.6"))
implementation(npm("three-csg-ts", "3.1.9"))
}

View File

@ -167,7 +167,7 @@ public class ThreeCanvas(
}
//Clipping planes
options.useProperty(Canvas3DOptions::clipping){clipping ->
options.useProperty(Canvas3DOptions::clipping) { clipping ->
if (!clipping.meta.isEmpty()) {
renderer.localClippingEnabled = true
boundingBox?.let { boundingBox ->
@ -192,7 +192,7 @@ public class ThreeCanvas(
}
}
options.useProperty(Canvas3DOptions::size){
options.useProperty(Canvas3DOptions::size) {
canvas.style.apply {
minWidth = "${options.size.minWith.toInt()}px"
maxWidth = "${options.size.maxWith.toInt()}px"
@ -273,18 +273,14 @@ public class ThreeCanvas(
return
}
if (this is Mesh) {
if (highlight) {
val edges = LineSegments(
val edges = getObjectByName(edgesName) ?: LineSegments(
EdgesGeometry(geometry),
material
).apply {
name = edgesName
}
add(edges)
} else {
val highlightEdges = children.find { it.name == edgesName }
highlightEdges?.let { remove(it) }
).also {
it.name = edgesName
add(it)
}
edges.visible = highlight
} else {
children.filter { it.name != edgesName }.forEach {
it.toggleHighlight(highlight, edgesName, material)

View File

@ -1,7 +1,6 @@
package space.kscience.visionforge.solid.three
import CSG
import info.laht.threekt.core.Object3D
import info.laht.threekt.objects.Mesh
import space.kscience.dataforge.names.startsWith
import space.kscience.visionforge.onPropertyChange
@ -38,11 +37,11 @@ public class ThreeCompositeFactory(public val three: ThreePlugin) : ThreeFactory
override val type: KClass<in Composite> get() = Composite::class
override fun invoke(three: ThreePlugin, obj: Composite): Object3D {
override fun invoke(three: ThreePlugin, obj: Composite): Mesh {
val first = three.buildObject3D(obj.first) as? Mesh ?: error("First part of composite is not a mesh")
val second = three.buildObject3D(obj.second) as? Mesh ?: error("Second part of composite is not a mesh")
return when (obj.compositeType) {
CompositeType.SUM, CompositeType.UNION -> CSG.union(first, second)
CompositeType.GROUP, CompositeType.UNION -> CSG.union(first, second)
CompositeType.INTERSECT -> CSG.intersect(first, second)
CompositeType.SUBTRACT -> CSG.subtract(first, second)
}.apply {