From dba08ec39fe6f22b04fcb7ba5f4c48af34123740 Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 22 Sep 2022 15:50:22 +0200 Subject: [PATCH 01/63] refactor Hdf5Access --- .../datastore/services/MeshFileService.scala | 66 +++++++++---------- .../datastore/storage/Hdf5FileCache.scala | 8 ++- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index d8942ad1bed..eba3ead2034 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -1,21 +1,21 @@ package com.scalableminds.webknossos.datastore.services -import java.nio.file.{Path, Paths} - import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.DataStoreConfig import com.scalableminds.webknossos.datastore.storage.{CachedHdf5File, Hdf5FileCache} import com.typesafe.scalalogging.LazyLogging -import javax.inject.Inject import net.liftweb.common.Box import net.liftweb.util.Helpers.tryo import org.apache.commons.io.FilenameUtils import play.api.libs.json.{Json, OFormat} +import java.nio.file.{Path, Paths} +import javax.inject.Inject import scala.collection.JavaConverters._ import scala.concurrent.ExecutionContext +import scala.util.Using trait GenericJsonFormat[T] {} @@ -84,14 +84,11 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC Note that null is a valid value here for once. Meshfiles with no information about the meshFilePath will return Fox.empty, while meshfiles with one marked as empty, will return Fox.successful(null) */ + def mappingNameForMeshFile(meshFilePath: Path): Fox[String] = - for { - cachedMeshFile <- tryo { meshFileCache.withCache(meshFilePath)(CachedHdf5File.fromPath) } ?~> "mesh.file.open.failed" - mappingName <- tryo { _: Throwable => - cachedMeshFile.finishAccess() - } { cachedMeshFile.reader.string().getAttr("/", "metadata/mapping_name") } ?~> "mesh.file.readEncoding.failed" - _ = cachedMeshFile.finishAccess() - } yield mappingName + safeExecute(meshFilePath) { cachedMeshFile => + cachedMeshFile.reader.string().getAttr("/", "metadata/mapping_name") + } ?~> "mesh.file.readEncoding.failed" def listMeshChunksForSegment(organizationName: String, dataSetName: String, @@ -105,20 +102,14 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") - for { - cachedMeshFile <- tryo { meshFileCache.withCache(meshFilePath)(CachedHdf5File.fromPath) } ?~> "mesh.file.open.failed" - chunkPositionLiterals <- tryo { _: Throwable => - cachedMeshFile.finishAccess() - } { - cachedMeshFile.reader - .`object`() - .getAllGroupMembers(s"/${listMeshChunksRequest.segmentId}/$defaultLevelOfDetail") - .asScala - .toList - }.toFox - _ = cachedMeshFile.finishAccess() - positions <- Fox.serialCombined(chunkPositionLiterals)(parsePositionLiteral) - } yield positions + safeExecute(meshFilePath) { cachedMeshFile => + val chunkPositionLiterals = cachedMeshFile.reader + .`object`() + .getAllGroupMembers(s"/${listMeshChunksRequest.segmentId}/$defaultLevelOfDetail") + .asScala + .toList + Fox.serialCombined(chunkPositionLiterals)(parsePositionLiteral) + }.flatten ?~> "mesh.file.open.failed" } def readMeshChunk(organizationName: String, @@ -131,19 +122,24 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(dataLayerName) .resolve(meshesDir) .resolve(s"${meshChunkDataRequest.meshFile}.$meshFileExtension") - for { - cachedMeshFile <- tryo { meshFileCache.withCache(meshFilePath)(CachedHdf5File.fromPath) } ?~> "mesh.file.open.failed" - encoding <- tryo { _: Throwable => - cachedMeshFile.finishAccess() - } { cachedMeshFile.reader.string().getAttr("/", "metadata/encoding") } ?~> "mesh.file.readEncoding.failed" - key = s"/${meshChunkDataRequest.segmentId}/$defaultLevelOfDetail/${positionLiteral(meshChunkDataRequest.position)}" - data <- tryo { _: Throwable => - cachedMeshFile.finishAccess() - } { cachedMeshFile.reader.readAsByteArray(key) } ?~> "mesh.file.readData.failed" - _ = cachedMeshFile.finishAccess() - } yield (data, encoding) + + safeExecute(meshFilePath) { cachedMeshFile => + val encoding = cachedMeshFile.reader.string().getAttr("/", "metadata/encoding") + val key = + s"/${meshChunkDataRequest.segmentId}/$defaultLevelOfDetail/${positionLiteral(meshChunkDataRequest.position)}" + val data = cachedMeshFile.reader.readAsByteArray(key) + (data, encoding) + } ?~> "mesh.file.readData.failed" } + private def safeExecute[T](filePath: Path)(block: CachedHdf5File => T): Fox[T] = + for { + _ <- bool2Fox(filePath.toFile.exists()) ?~> "mesh.file.open.failed" + result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { + block + }.toFox + } yield result + private def positionLiteral(position: Vec3Int) = s"${position.x}_${position.y}_${position.z}" diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala index bab2be48413..7a76d160306 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala @@ -1,13 +1,15 @@ package com.scalableminds.webknossos.datastore.storage -import java.nio.file.Path - import ch.systemsx.cisd.hdf5.{HDF5FactoryProvider, IHDF5Reader} import com.scalableminds.util.cache.LRUConcurrentCache import com.scalableminds.webknossos.datastore.dataformats.SafeCachable -case class CachedHdf5File(reader: IHDF5Reader) extends SafeCachable { +import java.nio.file.Path + +case class CachedHdf5File(reader: IHDF5Reader) extends SafeCachable with AutoCloseable { override protected def onFinalize(): Unit = reader.close() + + override def close(): Unit = this.finishAccess() } object CachedHdf5File { From c9aecb77d72d9963fc69c92aa41b028de2f676b9 Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 22 Sep 2022 19:30:15 +0200 Subject: [PATCH 02/63] add first new mesh file functionality --- .../datastore/services/MeshFileService.scala | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index eba3ead2034..67f3bba5647 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -1,5 +1,6 @@ package com.scalableminds.webknossos.datastore.services +import com.google.common.io.LittleEndianDataInputStream import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.{Fox, FoxImplicits} @@ -11,6 +12,7 @@ import net.liftweb.util.Helpers.tryo import org.apache.commons.io.FilenameUtils import play.api.libs.json.{Json, OFormat} +import java.io.ByteArrayInputStream import java.nio.file.{Path, Paths} import javax.inject.Inject import scala.collection.JavaConverters._ @@ -55,6 +57,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC private val meshesDir = "meshes" private val meshFileExtension = "hdf5" private val defaultLevelOfDetail = 0 + private def hashFn: Long => Long = identity private lazy val meshFileCache = new Hdf5FileCache(30) @@ -112,6 +115,107 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC }.flatten ?~> "mesh.file.open.failed" } + def listMeshChunksForSegmentNewFormat(organizationName: String, + dataSetName: String, + dataLayerName: String, + listMeshChunksRequest: ListMeshChunksRequest): Fox[List[Vec3Int]] = { + val meshFilePath = + dataBaseDir + .resolve(organizationName) + .resolve(dataSetName) + .resolve(dataLayerName) + .resolve(meshesDir) + .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") + + safeExecute(meshFilePath) { cachedMeshFile => +// val chunkPositionLiterals = cachedMeshFile.reader +// .`object`() +// .getAllGroupMembers(s"/${listMeshChunksRequest.segmentId}/$defaultLevelOfDetail") +// .asScala +// .toList +// Fox.serialCombined(chunkPositionLiterals)(parsePositionLiteral) + val segmentId = listMeshChunksRequest.segmentId + val (neuroglancerStart, neuroglancerEnd) = getNeuroglancerOffsets(segmentId, cachedMeshFile) + val manifest = cachedMeshFile.reader + .uint8() + .readArrayBlockWithOffset("neuroglancer", (neuroglancerEnd - neuroglancerStart).toInt, neuroglancerStart) + val byteInput = new ByteArrayInputStream(manifest) + val dis = new LittleEndianDataInputStream(byteInput) + // todo use vec3int + val chunkShape = new Array[Float](3) + for (d <- 0 until 3) { + chunkShape(d) = dis.readFloat + } + // todo use vec3int + val gridOrigin = new Array[Float](3) + for (d <- 0 until 3) { + gridOrigin(d) = dis.readFloat + } + // TODO should uint + val numLods = dis.readInt() + val lodScales = new Array[Float](numLods) + for (d <- 0 until numLods) { + lodScales(d) = dis.readFloat() + } + // TODO use vec3int + val vertexOffsets = new Array[Array[Float]](numLods) + for (d <- 0 until numLods) { + for (x <- 0 until 3) { + vertexOffsets(d)(x) = dis.readFloat() + } + } + // TODO should be uint + val numFragmentsPerLod = new Array[Int](numLods) + for (lod <- 0 until numLods) { + numFragmentsPerLod(lod) = dis.readInt() + } + // TODO should be uint + val fragmentPositions = new Array[Array[Array[Int]]](numLods) + val fragmentPositionsVec3 = new Array[Array[Vec3Int]](numLods) + val fragmentOffsets = new Array[Array[Int]](numLods) + for (lod <- 0 until numLods) { + // TODO is that the right order?? + for (row <- 0 until 3) { + for (col <- 0 until numFragmentsPerLod(lod)) { + fragmentPositions(lod)(row)(col) = dis.readInt() + } + } + // TODO make functional, this is a mess + for (col <- 0 until numFragmentsPerLod(lod)) { + fragmentPositionsVec3(lod)(col) = + Vec3Int(fragmentPositions(lod)(0)(col), fragmentPositions(lod)(1)(col), fragmentPositions(lod)(2)(col)) + } + + for (row <- 0 until numFragmentsPerLod(lod)) { + fragmentOffsets(lod)(row) = dis.readInt() + } + } + val DEFAULT_LOD = 1 + fragmentPositionsVec3(DEFAULT_LOD).toList + } + } + + private def getNeuroglancerOffsets(segmentId: Long, cachedMeshFile: CachedHdf5File): (Long, Long) = { + val nBuckets = cachedMeshFile.reader.uint64().getAttr("/", "metadata/n_buckets") + // TODO get hashfunction from metadata + val bucketIndex = hashFn(segmentId) % nBuckets + val cappedBucketIndex = bucketIndex.toInt + val bucketOffsets = cachedMeshFile.reader.uint64().readArrayBlockWithOffset("bucket_offsets", 2, bucketIndex) + val bucketStart = bucketOffsets(cappedBucketIndex) + val cappedBucketStart = bucketStart.toInt + val bucketEnd = bucketOffsets(cappedBucketIndex + 1) + val cappedBucketEnd = bucketEnd.toInt + // TODO is this access correct? + val buckets = cachedMeshFile.reader + .uint64() + .readMatrixBlockWithOffset("buckets", cappedBucketEnd - cappedBucketStart + 1, 3, bucketStart, 0) + // TODO does this work as intended? + val bucketLocalOffset = buckets.map(_(0)).indexOf(segmentId) + val neuroglancerStart = buckets(bucketLocalOffset)(1) + val neuroglancerEnd = buckets(bucketLocalOffset)(2) + (neuroglancerStart, neuroglancerEnd) + } + def readMeshChunk(organizationName: String, dataSetName: String, dataLayerName: String, From 1f60390e30b3c6abcc350fc6c9a8e2382d3163a1 Mon Sep 17 00:00:00 2001 From: leowe Date: Fri, 23 Sep 2022 16:38:58 +0200 Subject: [PATCH 03/63] finished basic functionality --- .../util/geometry/Vec3Float.scala | 9 + .../scalableminds/util/geometry/Vec3Int.scala | 2 + .../datastore/services/MeshFileService.scala | 182 ++++++++++++------ 3 files changed, 137 insertions(+), 56 deletions(-) create mode 100644 util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala new file mode 100644 index 00000000000..cc645bea0be --- /dev/null +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala @@ -0,0 +1,9 @@ +package com.scalableminds.util.geometry; + +case class Vec3Float(x: Float, y: Float, z: Float) { + def scale(s: Float): Vec3Float = Vec3Float(x * s, y * s, z * s) + + def *(s: Float): Vec3Float = scale(s) + def *(that: Vec3Float): Vec3Float = Vec3Float(x * that.x, y * that.y, z * that.z) + def +(that: Vec3Float): Vec3Float = Vec3Float(x + that.x, y + that.y, z + that.y) +} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala index 87e5754d949..6c3c8f65e6f 100644 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala @@ -32,6 +32,8 @@ case class Vec3Int(x: Int, y: Int, z: Int) { def toList: List[Int] = List(x, y, z) + def toVec3Float: Vec3Float = Vec3Float(x.toFloat, y.toFloat, z.toFloat) + def move(dx: Int, dy: Int, dz: Int): Vec3Int = Vec3Int(x + dx, y + dy, z + dz) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 67f3bba5647..11df7ae12b6 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -1,7 +1,7 @@ package com.scalableminds.webknossos.datastore.services import com.google.common.io.LittleEndianDataInputStream -import com.scalableminds.util.geometry.Vec3Int +import com.scalableminds.util.geometry.{Vec3Float, Vec3Int} import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.DataStoreConfig @@ -49,6 +49,21 @@ object MeshFileNameWithMappingName { implicit val jsonFormat: OFormat[MeshFileNameWithMappingName] = Json.format[MeshFileNameWithMappingName] } +case class NeuroglancerSegmentInfo(chunkShape: Vec3Float, + gridOrigin: Vec3Float, + numLods: Int, + lodScales: Array[Float], + vertexOffsets: Array[Vec3Float], + numFragmentsPerLod: Array[Int], + fragmentPositions: Array[Array[Vec3Int]], + fragmentOffsets: Array[Array[Int]]) + +case class MeshfileFragment(position: Vec3Float, byteOffset: Int, byteSize: Int) + +case class MeshfileLod(scale: Int, vertexOffset: Vec3Float, chunkShape: Vec3Float, fragments: List[MeshfileFragment]) + +case class Meshfile(chunkShape: Vec3Float, gridOrigin: Vec3Float, lods: List[MeshfileLod]) + class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionContext) extends FoxImplicits with LazyLogging { @@ -128,71 +143,107 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") safeExecute(meshFilePath) { cachedMeshFile => -// val chunkPositionLiterals = cachedMeshFile.reader -// .`object`() -// .getAllGroupMembers(s"/${listMeshChunksRequest.segmentId}/$defaultLevelOfDetail") -// .asScala -// .toList -// Fox.serialCombined(chunkPositionLiterals)(parsePositionLiteral) val segmentId = listMeshChunksRequest.segmentId val (neuroglancerStart, neuroglancerEnd) = getNeuroglancerOffsets(segmentId, cachedMeshFile) + // TODO transform is actually a matrix + val transform = cachedMeshFile.reader.float32().getAttr("/", "metadata/transform") val manifest = cachedMeshFile.reader .uint8() .readArrayBlockWithOffset("neuroglancer", (neuroglancerEnd - neuroglancerStart).toInt, neuroglancerStart) - val byteInput = new ByteArrayInputStream(manifest) - val dis = new LittleEndianDataInputStream(byteInput) - // todo use vec3int - val chunkShape = new Array[Float](3) - for (d <- 0 until 3) { - chunkShape(d) = dis.readFloat - } - // todo use vec3int - val gridOrigin = new Array[Float](3) - for (d <- 0 until 3) { - gridOrigin(d) = dis.readFloat - } - // TODO should uint - val numLods = dis.readInt() - val lodScales = new Array[Float](numLods) - for (d <- 0 until numLods) { - lodScales(d) = dis.readFloat() - } - // TODO use vec3int - val vertexOffsets = new Array[Array[Float]](numLods) - for (d <- 0 until numLods) { - for (x <- 0 until 3) { - vertexOffsets(d)(x) = dis.readFloat() - } - } - // TODO should be uint - val numFragmentsPerLod = new Array[Int](numLods) - for (lod <- 0 until numLods) { - numFragmentsPerLod(lod) = dis.readInt() - } - // TODO should be uint - val fragmentPositions = new Array[Array[Array[Int]]](numLods) - val fragmentPositionsVec3 = new Array[Array[Vec3Int]](numLods) - val fragmentOffsets = new Array[Array[Int]](numLods) - for (lod <- 0 until numLods) { - // TODO is that the right order?? - for (row <- 0 until 3) { - for (col <- 0 until numFragmentsPerLod(lod)) { - fragmentPositions(lod)(row)(col) = dis.readInt() - } - } - // TODO make functional, this is a mess + val segmentInfo = parseNeuroglancerManifest(manifest) + val meshfile = getFragmentsFromSegmentInfo(segmentInfo, transform, neuroglancerStart) + // TODO we need more info than just this, frontend probably needs also quantization bit + segmentInfo.fragmentPositions(defaultLevelOfDetail).toList + } + } + + private def getFragmentsFromSegmentInfo(segmentInfo: NeuroglancerSegmentInfo, + transform: Float, + neuroglancerOffsetStart: Long): Meshfile = { + val totalMeshSize = segmentInfo.fragmentOffsets.reduce((a, b) => a.sum + b.sum) + val meshByteStartOffset = neuroglancerOffsetStart - totalMeshSize + val fragmentByteOffsets = segmentInfo.fragmentOffsets.map(_.scanLeft(0)(_ + _)) + + def computeGlobalPositionAndOffset(lod: Int, currentFragment: Int): MeshfileFragment = { + val globalPosition = segmentInfo.gridOrigin + segmentInfo + .fragmentPositions(lod)(currentFragment) + .toVec3Float * segmentInfo.chunkShape * segmentInfo.lodScales(lod) + + MeshfileFragment( + position = globalPosition * transform, + byteOffset = meshByteStartOffset.toInt + fragmentByteOffsets(lod)(currentFragment), + byteSize = segmentInfo.fragmentOffsets(lod)(currentFragment), + ) + } + + val lods = for (lod <- 0 until segmentInfo.numLods) yield lod + + def fragmentNums(lod: Int): IndexedSeq[(Int, Int)] = + for (currentFragment <- 0 until segmentInfo.numFragmentsPerLod(lod)) + yield (lod, currentFragment) + val fragments = lods.map(lod => fragmentNums(lod).map(x => computeGlobalPositionAndOffset(x._1, x._2)).toList) + + val meshfileLods = lods.map( + lod => + MeshfileLod(scale = segmentInfo.lodScales(lod).toInt, + vertexOffset = segmentInfo.vertexOffsets(lod), + chunkShape = segmentInfo.chunkShape, + fragments = fragments(lod))).toList + Meshfile(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods) + } + private def parseNeuroglancerManifest(manifest: Array[Byte]): NeuroglancerSegmentInfo = { + // All Ints here should be UInt32 per spec. + // But they all are used to index into Arrays and JVM doesn't allow for Long Array Indexes, + // we can't convert them. + // TODO Check whether limit exceeded for the Ints. + val byteInput = new ByteArrayInputStream(manifest) + val dis = new LittleEndianDataInputStream(byteInput) + + val chunkShape = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) + val gridOrigin = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) + + val numLods = dis.readInt + + val lodScales = new Array[Float](numLods) + for (d <- 0 until numLods) { + lodScales(d) = dis.readFloat + } + + val vertexOffsets = new Array[Vec3Float](numLods) + for (d <- 0 until numLods) { + vertexOffsets(d) = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) + } + + val numFragmentsPerLod = new Array[Int](numLods) + for (lod <- 0 until numLods) { + numFragmentsPerLod(lod) = dis.readInt() + } + + val fragmentPositions = new Array[Array[Array[Int]]](numLods) + val fragmentPositionsVec3 = new Array[Array[Vec3Int]](numLods) + val fragmentSizes = new Array[Array[Int]](numLods) + for (lod <- 0 until numLods) { + for (row <- 0 until 3) { for (col <- 0 until numFragmentsPerLod(lod)) { - fragmentPositionsVec3(lod)(col) = - Vec3Int(fragmentPositions(lod)(0)(col), fragmentPositions(lod)(1)(col), fragmentPositions(lod)(2)(col)) + fragmentPositions(lod)(row)(col) = dis.readInt } + } - for (row <- 0 until numFragmentsPerLod(lod)) { - fragmentOffsets(lod)(row) = dis.readInt() - } + fragmentPositionsVec3(lod) = fragmentPositions(lod).transpose.map(xs => Vec3Int(xs(0), xs(1), xs(2))) + + for (fragmentNum <- 0 until numFragmentsPerLod(lod)) { + fragmentSizes(lod)(fragmentNum) = dis.readInt } - val DEFAULT_LOD = 1 - fragmentPositionsVec3(DEFAULT_LOD).toList } + + NeuroglancerSegmentInfo(chunkShape, + gridOrigin, + numLods, + lodScales, + vertexOffsets, + numFragmentsPerLod, + fragmentPositionsVec3, + fragmentSizes) } private def getNeuroglancerOffsets(segmentId: Long, cachedMeshFile: CachedHdf5File): (Long, Long) = { @@ -236,6 +287,25 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC } ?~> "mesh.file.readData.failed" } + + def readMeshChunkNewFormat(organizationName: String, + dataSetName: String, + dataLayerName: String, + meshChunkDataRequest: MeshChunkDataRequest, fragmentStartOffset: Long, fragmentSize: Int): Fox[(Array[Byte], String)] = { + val meshFilePath = dataBaseDir + .resolve(organizationName) + .resolve(dataSetName) + .resolve(dataLayerName) + .resolve(meshesDir) + .resolve(s"${meshChunkDataRequest.meshFile}.$meshFileExtension") + + safeExecute(meshFilePath) { cachedMeshFile => + val meshFormat = cachedMeshFile.reader.string().getAttr("/", "metadata/mesh_format") + val data = cachedMeshFile.reader.uint8().readArrayBlockWithOffset("neuroglancer", fragmentSize, fragmentStartOffset) + (data, meshFormat) + } ?~> "mesh.file.readData.failed" + } + private def safeExecute[T](filePath: Path)(block: CachedHdf5File => T): Fox[T] = for { _ <- bool2Fox(filePath.toFile.exists()) ?~> "mesh.file.open.failed" From 41904404d014871b8cbd5906e7dff6139aa67bbc Mon Sep 17 00:00:00 2001 From: leowe Date: Mon, 26 Sep 2022 18:45:06 +0200 Subject: [PATCH 04/63] write new routes --- .../util/geometry/Vec3Float.scala | 19 +- .../controllers/DataSourceController.scala | 60 ++++- .../datastore/services/MeshFileService.scala | 209 ++++++++++++------ ....scalableminds.webknossos.datastore.routes | 7 +- 4 files changed, 214 insertions(+), 81 deletions(-) diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala index cc645bea0be..f9557fb68e6 100644 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala @@ -1,4 +1,6 @@ -package com.scalableminds.util.geometry; +package com.scalableminds.util.geometry + +import play.api.libs.json.{Json, OFormat}; case class Vec3Float(x: Float, y: Float, z: Float) { def scale(s: Float): Vec3Float = Vec3Float(x * s, y * s, z * s) @@ -6,4 +8,19 @@ case class Vec3Float(x: Float, y: Float, z: Float) { def *(s: Float): Vec3Float = scale(s) def *(that: Vec3Float): Vec3Float = Vec3Float(x * that.x, y * that.y, z * that.z) def +(that: Vec3Float): Vec3Float = Vec3Float(x + that.x, y + that.y, z + that.y) + + def toList: List[Float] = List(x, y, z) + def matmul(that: Array[Array[Float]]): Option[Vec3Float] = { + val l: Array[Float] = this.toList.toArray ++ Array[Float](1.0.toFloat) + val result: Array[Array[Float]] = that.map(xs => xs.zip(l).map{case (x,y) => x * y}) + + if (that.length < 3 || that(0).length < 3) + None + else + Some(Vec3Float(result(0).sum, result(1).sum, result(2).sum)) + } +} + +object Vec3Float { + implicit val jsonFormat: OFormat[Vec3Float] = Json.format[Vec3Float] } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala index 63a9db7e333..7ed93fc8abd 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala @@ -415,15 +415,15 @@ Expects: } @ApiOperation(hidden = true, value = "") - def listMeshChunksForSegment(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String): Action[ListMeshChunksRequest] = + def listMeshChunksForSegmentV0(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String): Action[ListMeshChunksRequest] = Action.async(validateJson[ListMeshChunksRequest]) { implicit request => accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), urlOrHeaderToken(token, request)) { for { - positions <- meshFileService.listMeshChunksForSegment(organizationName, + positions <- meshFileService.listMeshChunksForSegmentV0(organizationName, dataSetName, dataLayerName, request.body) ?~> Messages( @@ -435,15 +435,53 @@ Expects: } @ApiOperation(hidden = true, value = "") - def readMeshChunk(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String): Action[MeshChunkDataRequest] = - Action.async(validateJson[MeshChunkDataRequest]) { implicit request => + def listMeshChunksForSegmentV1(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String): Action[ListMeshChunksRequest] = + Action.async(validateJson[ListMeshChunksRequest]) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), + urlOrHeaderToken(token, request)) { + for { + positions <- meshFileService.listMeshChunksForSegmentV1(organizationName, + dataSetName, + dataLayerName, + request.body) ?~> Messages( + "mesh.file.listChunks.failed", + request.body.segmentId.toString, + request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST + } yield Ok(Json.toJson(positions)) + } + } + + @ApiOperation(hidden = true, value = "") + def readMeshChunkV0(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String): Action[MeshChunkDataRequestV0] = + Action.async(validateJson[MeshChunkDataRequestV0]) { implicit request => accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), urlOrHeaderToken(token, request)) { for { - (data, encoding) <- meshFileService.readMeshChunk(organizationName, dataSetName, dataLayerName, request.body) ?~> "mesh.file.loadChunk.failed" + (data, encoding) <- meshFileService.readMeshChunkV0(organizationName, dataSetName, dataLayerName, request.body) ?~> "mesh.file.loadChunk.failed" + } yield { + if (encoding.contains("gzip")) { + Ok(data).withHeaders("Content-Encoding" -> "gzip") + } else Ok(data) + } + } + } + + @ApiOperation(hidden = true, value = "") + def readMeshChunkV1(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String): Action[MeshChunkDataRequestV1] = + Action.async(validateJson[MeshChunkDataRequestV1]) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), + urlOrHeaderToken(token, request)) { + for { + (data, encoding) <- meshFileService.readMeshChunkV1(organizationName, dataSetName, dataLayerName, request.body) ?~> "mesh.file.loadChunk.failed" } yield { if (encoding.contains("gzip")) { Ok(data).withHeaders("Content-Encoding" -> "gzip") diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 11df7ae12b6..3821a82d881 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -7,16 +7,18 @@ import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.DataStoreConfig import com.scalableminds.webknossos.datastore.storage.{CachedHdf5File, Hdf5FileCache} import com.typesafe.scalalogging.LazyLogging -import net.liftweb.common.Box +import net.liftweb.common.{Box, Empty, Full} import net.liftweb.util.Helpers.tryo +import org.apache.commons.codec.digest.MurmurHash3 import org.apache.commons.io.FilenameUtils import play.api.libs.json.{Json, OFormat} import java.io.ByteArrayInputStream +import java.nio.ByteBuffer import java.nio.file.{Path, Paths} import javax.inject.Inject import scala.collection.JavaConverters._ -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext} import scala.util.Using trait GenericJsonFormat[T] {} @@ -30,23 +32,34 @@ object ListMeshChunksRequest { implicit val jsonFormat: OFormat[ListMeshChunksRequest] = Json.format[ListMeshChunksRequest] } -case class MeshChunkDataRequest( +case class MeshChunkDataRequestV0( meshFile: String, position: Vec3Int, segmentId: Long ) -object MeshChunkDataRequest { - implicit val jsonFormat: OFormat[MeshChunkDataRequest] = Json.format[MeshChunkDataRequest] +case class MeshChunkDataRequestV1( + meshFile: String, + fragmentStartOffset: Long, + fragmentSize: Int +) + +object MeshChunkDataRequestV0 { + implicit val jsonFormat: OFormat[MeshChunkDataRequestV0] = Json.format[MeshChunkDataRequestV0] +} + +object MeshChunkDataRequestV1 { + implicit val jsonFormat: OFormat[MeshChunkDataRequestV1] = Json.format[MeshChunkDataRequestV1] } -case class MeshFileNameWithMappingName( +case class MeshFileInfo( meshFileName: String, - mappingName: Option[String] + mappingName: Option[String], + formatVersion: Int ) -object MeshFileNameWithMappingName { - implicit val jsonFormat: OFormat[MeshFileNameWithMappingName] = Json.format[MeshFileNameWithMappingName] +object MeshFileInfo { + implicit val jsonFormat: OFormat[MeshFileInfo] = Json.format[MeshFileInfo] } case class NeuroglancerSegmentInfo(chunkShape: Vec3Float, @@ -58,11 +71,26 @@ case class NeuroglancerSegmentInfo(chunkShape: Vec3Float, fragmentPositions: Array[Array[Vec3Int]], fragmentOffsets: Array[Array[Int]]) -case class MeshfileFragment(position: Vec3Float, byteOffset: Int, byteSize: Int) +case class MeshFragment(position: Vec3Float, byteOffset: Int, byteSize: Int) -case class MeshfileLod(scale: Int, vertexOffset: Vec3Float, chunkShape: Vec3Float, fragments: List[MeshfileFragment]) +object MeshFragment { + implicit val jsonFormat: OFormat[MeshFragment] = Json.format[MeshFragment] +} +case class MeshLodInfo(scale: Int, vertexOffset: Vec3Float, chunkShape: Vec3Float, fragments: List[MeshFragment]) + +object MeshLodInfo { + implicit val jsonFormat: OFormat[MeshLodInfo] = Json.format[MeshLodInfo] +} +case class MeshSegmentInfo(chunkShape: Vec3Float, gridOrigin: Vec3Float, lods: List[MeshLodInfo]) -case class Meshfile(chunkShape: Vec3Float, gridOrigin: Vec3Float, lods: List[MeshfileLod]) +object MeshSegmentInfo { + implicit val jsonFormat: OFormat[MeshSegmentInfo] = Json.format[MeshSegmentInfo] +} +case class WebknossosSegmentInfo(transform: Array[Array[Float]], meshFormat: String, chunks: MeshSegmentInfo) + +object WebknossosSegmentInfo { + implicit val jsonFormat: OFormat[WebknossosSegmentInfo] = Json.format[WebknossosSegmentInfo] +} class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionContext) extends FoxImplicits @@ -72,13 +100,16 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC private val meshesDir = "meshes" private val meshFileExtension = "hdf5" private val defaultLevelOfDetail = 0 - private def hashFn: Long => Long = identity + private def getHashFunction(name: String): Long => Long = name match { + case "identity" => identity + case "murmurhash3_x64_128" => + x: Long => + Math.abs(MurmurHash3.hash128x64(ByteBuffer.allocate(8).putLong(x).array())(1)) + } private lazy val meshFileCache = new Hdf5FileCache(30) - def exploreMeshFiles(organizationName: String, - dataSetName: String, - dataLayerName: String): Fox[Set[MeshFileNameWithMappingName]] = { + def exploreMeshFiles(organizationName: String, dataSetName: String, dataLayerName: String): Fox[Set[MeshFileInfo]] = { val layerDir = dataBaseDir.resolve(organizationName).resolve(dataSetName).resolve(dataLayerName) val meshFileNames = PathUtils .listFiles(layerDir.resolve(meshesDir), PathUtils.fileExtensionFilter(meshFileExtension)) @@ -91,27 +122,38 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val meshFilePath = layerDir.resolve(meshesDir).resolve(s"$fileName.$meshFileExtension") mappingNameForMeshFile(meshFilePath) } + + val mappingVersions = meshFileNames.map { fileName => + val meshFilePath = layerDir.resolve(meshesDir).resolve(s"$fileName.$meshFileExtension") + mappingVersionForMeshFile(meshFilePath) + } + for { mappingNameBoxes: Seq[Box[String]] <- Fox.sequence(mappingNameFoxes) + mappingNameOptions = mappingNameBoxes.map(_.toOption) - zipped = meshFileNames.zip(mappingNameOptions).toSet - } yield zipped.map(tuple => MeshFileNameWithMappingName(tuple._1, tuple._2)) + zipped = (meshFileNames, mappingNameOptions, mappingVersions).zipped + } yield zipped.map(MeshFileInfo(_, _, _)).toSet } /* Note that null is a valid value here for once. Meshfiles with no information about the meshFilePath will return Fox.empty, while meshfiles with one marked as empty, will return Fox.successful(null) */ - def mappingNameForMeshFile(meshFilePath: Path): Fox[String] = safeExecute(meshFilePath) { cachedMeshFile => cachedMeshFile.reader.string().getAttr("/", "metadata/mapping_name") } ?~> "mesh.file.readEncoding.failed" - def listMeshChunksForSegment(organizationName: String, - dataSetName: String, - dataLayerName: String, - listMeshChunksRequest: ListMeshChunksRequest): Fox[List[Vec3Int]] = { + def mappingVersionForMeshFile(meshFilePath: Path): Int = + safeExecuteBox(meshFilePath) { cachedMeshFile => + cachedMeshFile.reader.int32().getAttr("/", "metadata/version") + }.toOption.getOrElse(0) + + def listMeshChunksForSegmentV0(organizationName: String, + dataSetName: String, + dataLayerName: String, + listMeshChunksRequest: ListMeshChunksRequest): Fox[List[Vec3Int]] = { val meshFilePath = dataBaseDir .resolve(organizationName) @@ -130,10 +172,12 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC }.flatten ?~> "mesh.file.open.failed" } - def listMeshChunksForSegmentNewFormat(organizationName: String, - dataSetName: String, - dataLayerName: String, - listMeshChunksRequest: ListMeshChunksRequest): Fox[List[Vec3Int]] = { + def listMeshChunksForSegmentV1(organizationName: String, + dataSetName: String, + dataLayerName: String, + listMeshChunksRequest: ListMeshChunksRequest): Fox[WebknossosSegmentInfo] = { + // TODO neue routen bauen und entsprechende datentypen zurückgeben + val meshFilePath = dataBaseDir .resolve(organizationName) @@ -145,35 +189,42 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC safeExecute(meshFilePath) { cachedMeshFile => val segmentId = listMeshChunksRequest.segmentId val (neuroglancerStart, neuroglancerEnd) = getNeuroglancerOffsets(segmentId, cachedMeshFile) - // TODO transform is actually a matrix - val transform = cachedMeshFile.reader.float32().getAttr("/", "metadata/transform") + val transform = cachedMeshFile.reader.float32().readMatrixBlockWithOffset("metadata/transform", 4, 3, 0, 0) + val encoding = cachedMeshFile.reader.string().getAttr("/", "metadata/mesh_format") + + val lodScaleMultiplier = cachedMeshFile.reader.float32().getAttr("", "metadata/lod_scale_multiplier") val manifest = cachedMeshFile.reader .uint8() - .readArrayBlockWithOffset("neuroglancer", (neuroglancerEnd - neuroglancerStart).toInt, neuroglancerStart) + .readArrayBlockWithOffset("neuroglancer", (neuroglancerEnd - neuroglancerStart + 1).toInt, neuroglancerStart) val segmentInfo = parseNeuroglancerManifest(manifest) - val meshfile = getFragmentsFromSegmentInfo(segmentInfo, transform, neuroglancerStart) - // TODO we need more info than just this, frontend probably needs also quantization bit - segmentInfo.fragmentPositions(defaultLevelOfDetail).toList - } + val meshfile = getFragmentsFromSegmentInfo(segmentInfo, transform, lodScaleMultiplier, neuroglancerStart) + meshfile + .map(meshInfo => WebknossosSegmentInfo(transform = transform, meshFormat = encoding, chunks = meshInfo)) + .toFox + }.flatten } private def getFragmentsFromSegmentInfo(segmentInfo: NeuroglancerSegmentInfo, - transform: Float, - neuroglancerOffsetStart: Long): Meshfile = { - val totalMeshSize = segmentInfo.fragmentOffsets.reduce((a, b) => a.sum + b.sum) + transform: Array[Array[Float]], + lodScaleMultiplier: Float, + neuroglancerOffsetStart: Long): Option[MeshSegmentInfo] = { + val totalMeshSize = segmentInfo.fragmentOffsets.map(_.sum).sum val meshByteStartOffset = neuroglancerOffsetStart - totalMeshSize - val fragmentByteOffsets = segmentInfo.fragmentOffsets.map(_.scanLeft(0)(_ + _)) + val fragmentByteOffsets = segmentInfo.fragmentOffsets.map(_.scanLeft(0)(_ + _)) // This builds a cumulative sum - def computeGlobalPositionAndOffset(lod: Int, currentFragment: Int): MeshfileFragment = { + def computeGlobalPositionAndOffset(lod: Int, currentFragment: Int): Option[MeshFragment] = { val globalPosition = segmentInfo.gridOrigin + segmentInfo .fragmentPositions(lod)(currentFragment) - .toVec3Float * segmentInfo.chunkShape * segmentInfo.lodScales(lod) - - MeshfileFragment( - position = globalPosition * transform, - byteOffset = meshByteStartOffset.toInt + fragmentByteOffsets(lod)(currentFragment), - byteSize = segmentInfo.fragmentOffsets(lod)(currentFragment), - ) + .toVec3Float * segmentInfo.chunkShape * segmentInfo.lodScales(lod) * lodScaleMultiplier + val transformedPosition = globalPosition.matmul(transform) + + transformedPosition.map( + position => + MeshFragment( + position = position, + byteOffset = meshByteStartOffset.toInt + fragmentByteOffsets(lod)(currentFragment), + byteSize = segmentInfo.fragmentOffsets(lod)(currentFragment), + )) } val lods = for (lod <- 0 until segmentInfo.numLods) yield lod @@ -183,13 +234,20 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC yield (lod, currentFragment) val fragments = lods.map(lod => fragmentNums(lod).map(x => computeGlobalPositionAndOffset(x._1, x._2)).toList) - val meshfileLods = lods.map( - lod => - MeshfileLod(scale = segmentInfo.lodScales(lod).toInt, - vertexOffset = segmentInfo.vertexOffsets(lod), - chunkShape = segmentInfo.chunkShape, - fragments = fragments(lod))).toList - Meshfile(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods) + if (fragments.contains(None)) + None + else { + val meshfileLods = lods + .map( + lod => + MeshLodInfo(scale = segmentInfo.lodScales(lod).toInt, + vertexOffset = segmentInfo.vertexOffsets(lod), + chunkShape = segmentInfo.chunkShape, + fragments = fragments(lod).flatten)) + .toList + Some( + MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods)) + } } private def parseNeuroglancerManifest(manifest: Array[Byte]): NeuroglancerSegmentInfo = { // All Ints here should be UInt32 per spec. @@ -248,29 +306,29 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC private def getNeuroglancerOffsets(segmentId: Long, cachedMeshFile: CachedHdf5File): (Long, Long) = { val nBuckets = cachedMeshFile.reader.uint64().getAttr("/", "metadata/n_buckets") - // TODO get hashfunction from metadata - val bucketIndex = hashFn(segmentId) % nBuckets + val hashName = cachedMeshFile.reader.string().getAttr("/", "metadata/hash_function") + + val bucketIndex = getHashFunction(hashName)(segmentId) % nBuckets val cappedBucketIndex = bucketIndex.toInt val bucketOffsets = cachedMeshFile.reader.uint64().readArrayBlockWithOffset("bucket_offsets", 2, bucketIndex) val bucketStart = bucketOffsets(cappedBucketIndex) val cappedBucketStart = bucketStart.toInt val bucketEnd = bucketOffsets(cappedBucketIndex + 1) val cappedBucketEnd = bucketEnd.toInt - // TODO is this access correct? val buckets = cachedMeshFile.reader .uint64() .readMatrixBlockWithOffset("buckets", cappedBucketEnd - cappedBucketStart + 1, 3, bucketStart, 0) - // TODO does this work as intended? + val bucketLocalOffset = buckets.map(_(0)).indexOf(segmentId) val neuroglancerStart = buckets(bucketLocalOffset)(1) val neuroglancerEnd = buckets(bucketLocalOffset)(2) (neuroglancerStart, neuroglancerEnd) } - def readMeshChunk(organizationName: String, - dataSetName: String, - dataLayerName: String, - meshChunkDataRequest: MeshChunkDataRequest): Fox[(Array[Byte], String)] = { + def readMeshChunkV0(organizationName: String, + dataSetName: String, + dataLayerName: String, + meshChunkDataRequest: MeshChunkDataRequestV0): Fox[(Array[Byte], String)] = { val meshFilePath = dataBaseDir .resolve(organizationName) .resolve(dataSetName) @@ -287,11 +345,11 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC } ?~> "mesh.file.readData.failed" } - - def readMeshChunkNewFormat(organizationName: String, - dataSetName: String, - dataLayerName: String, - meshChunkDataRequest: MeshChunkDataRequest, fragmentStartOffset: Long, fragmentSize: Int): Fox[(Array[Byte], String)] = { + def readMeshChunkV1(organizationName: String, + dataSetName: String, + dataLayerName: String, + meshChunkDataRequest: MeshChunkDataRequestV1, + ): Fox[(Array[Byte], String)] = { val meshFilePath = dataBaseDir .resolve(organizationName) .resolve(dataSetName) @@ -301,7 +359,12 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC safeExecute(meshFilePath) { cachedMeshFile => val meshFormat = cachedMeshFile.reader.string().getAttr("/", "metadata/mesh_format") - val data = cachedMeshFile.reader.uint8().readArrayBlockWithOffset("neuroglancer", fragmentSize, fragmentStartOffset) + val data = + cachedMeshFile.reader + .uint8() + .readArrayBlockWithOffset("neuroglancer", + meshChunkDataRequest.fragmentSize, + meshChunkDataRequest.fragmentStartOffset) (data, meshFormat) } ?~> "mesh.file.readData.failed" } @@ -314,6 +377,18 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC }.toFox } yield result + private def safeExecuteBox[T](filePath: Path)(block: CachedHdf5File => T): Box[T] = + for { + _ <- if (filePath.toFile.exists()) { + new Full + } else { + Empty ~> "mesh.file.open.failed" + } + result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { + block + }.toOption + } yield result + private def positionLiteral(position: Vec3Int) = s"${position.x}_${position.y}_${position.z}" diff --git a/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes b/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes index 4848e40ac6f..7ef4caa6bd7 100644 --- a/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes +++ b/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes @@ -56,8 +56,11 @@ POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agg # Mesh files GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegment(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunk(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/v1/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentV1(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/v1/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkV1(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) + # Connectome files GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listConnectomeFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) From ca0690c74c5f8835ed3f977b8887c914bd428340 Mon Sep 17 00:00:00 2001 From: leowe Date: Mon, 26 Sep 2022 19:08:07 +0200 Subject: [PATCH 05/63] add small todo --- .../webknossos/datastore/services/MeshFileService.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 3821a82d881..e52b953688f 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -55,7 +55,7 @@ object MeshChunkDataRequestV1 { case class MeshFileInfo( meshFileName: String, mappingName: Option[String], - formatVersion: Int + formatVersion: Long ) object MeshFileInfo { @@ -216,6 +216,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val globalPosition = segmentInfo.gridOrigin + segmentInfo .fragmentPositions(lod)(currentFragment) .toVec3Float * segmentInfo.chunkShape * segmentInfo.lodScales(lod) * lodScaleMultiplier + // todo this is still the wrong way around val transformedPosition = globalPosition.matmul(transform) transformedPosition.map( From 0865d99ec8f2be7ecda42ee4a4a8fb60728043bb Mon Sep 17 00:00:00 2001 From: leowe Date: Tue, 27 Sep 2022 13:37:32 +0200 Subject: [PATCH 06/63] add dummy draco route --- binaryData/draco_file.bin | Bin 0 -> 251 bytes .../util/geometry/Vec3Float.scala | 10 +- .../controllers/DataSourceController.scala | 6 + .../datastore/services/MeshFileService.scala | 106 ++++++++++-------- ....scalableminds.webknossos.datastore.routes | 2 +- 5 files changed, 67 insertions(+), 57 deletions(-) create mode 100644 binaryData/draco_file.bin diff --git a/binaryData/draco_file.bin b/binaryData/draco_file.bin new file mode 100644 index 0000000000000000000000000000000000000000..96ec360ddbfef72332e9fde2f3bd34fa2ca6357d GIT binary patch literal 251 zcmV)B3}%UkOd?Jr8rgk~f~*4|bojJJO?w9xUW0^R z3@`#C*tQ#hN(6s|!VKUE+ChjrKt6sRiT;SkGr%vQ0OT2P z4>WH*xZ&j)8oLrO(-Mej0RZS9k%A#c*ra}V%gCn5Vd+#I4)}zbCxAUIlr|Fp5#K=r zD)LgeR*z(>1BSso@%I7L1|#h_yQgXe000000RIC30I#64LN8J}LwdNFLtsh!K?(<^ BTtNT; literal 0 HcmV?d00001 diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala index f9557fb68e6..40de93c024f 100644 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala @@ -6,19 +6,11 @@ case class Vec3Float(x: Float, y: Float, z: Float) { def scale(s: Float): Vec3Float = Vec3Float(x * s, y * s, z * s) def *(s: Float): Vec3Float = scale(s) + def *(s: Double): Vec3Float = scale(s.toFloat) def *(that: Vec3Float): Vec3Float = Vec3Float(x * that.x, y * that.y, z * that.z) def +(that: Vec3Float): Vec3Float = Vec3Float(x + that.x, y + that.y, z + that.y) def toList: List[Float] = List(x, y, z) - def matmul(that: Array[Array[Float]]): Option[Vec3Float] = { - val l: Array[Float] = this.toList.toArray ++ Array[Float](1.0.toFloat) - val result: Array[Array[Float]] = that.map(xs => xs.zip(l).map{case (x,y) => x * y}) - - if (that.length < 3 || that(0).length < 3) - None - else - Some(Vec3Float(result(0).sum, result(1).sum, result(2).sum)) - } } object Vec3Float { diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala index 7ed93fc8abd..541fd47c1b9 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala @@ -400,6 +400,12 @@ Expects: } } + def dummyDracoFile(): Action[AnyContent] = Action.async { implicit request => + for { + draco <- meshFileService.readDummyDraco() + } yield Ok(Json.toJson(draco)) + } + @ApiOperation(hidden = true, value = "") def listMeshFiles(token: Option[String], organizationName: String, diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index e52b953688f..2f6a6fcb5f6 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -15,10 +15,10 @@ import play.api.libs.json.{Json, OFormat} import java.io.ByteArrayInputStream import java.nio.ByteBuffer -import java.nio.file.{Path, Paths} +import java.nio.file.{Files, Path, Paths} import javax.inject.Inject import scala.collection.JavaConverters._ -import scala.concurrent.{ExecutionContext} +import scala.concurrent.ExecutionContext import scala.util.Using trait GenericJsonFormat[T] {} @@ -86,7 +86,7 @@ case class MeshSegmentInfo(chunkShape: Vec3Float, gridOrigin: Vec3Float, lods: L object MeshSegmentInfo { implicit val jsonFormat: OFormat[MeshSegmentInfo] = Json.format[MeshSegmentInfo] } -case class WebknossosSegmentInfo(transform: Array[Array[Float]], meshFormat: String, chunks: MeshSegmentInfo) +case class WebknossosSegmentInfo(transform: Array[Array[Double]], meshFormat: String, chunks: MeshSegmentInfo) object WebknossosSegmentInfo { implicit val jsonFormat: OFormat[WebknossosSegmentInfo] = Json.format[WebknossosSegmentInfo] @@ -109,6 +109,11 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC private lazy val meshFileCache = new Hdf5FileCache(30) + def readDummyDraco(): Fox[Array[Byte]] = { + val path = dataBaseDir.resolve("draco_file.bin") + tryo(Files.readAllBytes(path)).toOption.toFox + } + def exploreMeshFiles(organizationName: String, dataSetName: String, dataLayerName: String): Fox[Set[MeshFileInfo]] = { val layerDir = dataBaseDir.resolve(organizationName).resolve(dataSetName).resolve(dataLayerName) val meshFileNames = PathUtils @@ -118,21 +123,22 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC } .toOption .getOrElse(Nil) - val mappingNameFoxes = meshFileNames.map { fileName => + + val meshFileVersions = meshFileNames.map { fileName => val meshFilePath = layerDir.resolve(meshesDir).resolve(s"$fileName.$meshFileExtension") - mappingNameForMeshFile(meshFilePath) + mappingVersionForMeshFile(meshFilePath) } - val mappingVersions = meshFileNames.map { fileName => + val mappingNameFoxes = (meshFileNames, meshFileVersions).zipped.map { (fileName, fileVersion) => val meshFilePath = layerDir.resolve(meshesDir).resolve(s"$fileName.$meshFileExtension") - mappingVersionForMeshFile(meshFilePath) + mappingNameForMeshFile(meshFilePath, fileVersion) } for { mappingNameBoxes: Seq[Box[String]] <- Fox.sequence(mappingNameFoxes) mappingNameOptions = mappingNameBoxes.map(_.toOption) - zipped = (meshFileNames, mappingNameOptions, mappingVersions).zipped + zipped = (meshFileNames, mappingNameOptions, meshFileVersions).zipped } yield zipped.map(MeshFileInfo(_, _, _)).toSet } @@ -140,14 +146,21 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC Note that null is a valid value here for once. Meshfiles with no information about the meshFilePath will return Fox.empty, while meshfiles with one marked as empty, will return Fox.successful(null) */ - def mappingNameForMeshFile(meshFilePath: Path): Fox[String] = - safeExecute(meshFilePath) { cachedMeshFile => - cachedMeshFile.reader.string().getAttr("/", "metadata/mapping_name") - } ?~> "mesh.file.readEncoding.failed" + def mappingNameForMeshFile(meshFilePath: Path, meshFileVersion: Long): Fox[String] = { + if (meshFileVersion == 0) { + safeExecute(meshFilePath) { cachedMeshFile => + cachedMeshFile.reader.string().getAttr("/", "metadata/mapping_name") + } ?~> "mesh.file.readEncoding.failed" + } else { + safeExecute(meshFilePath) { cachedMeshFile => + cachedMeshFile.reader.string().getAttr("/", "mapping_name") + } ?~> "mesh.file.readEncoding.failed" + } + } - def mappingVersionForMeshFile(meshFilePath: Path): Int = + def mappingVersionForMeshFile(meshFilePath: Path): Long = safeExecuteBox(meshFilePath) { cachedMeshFile => - cachedMeshFile.reader.int32().getAttr("/", "metadata/version") + cachedMeshFile.reader.int64().getAttr("/", "version") }.toOption.getOrElse(0) def listMeshChunksForSegmentV0(organizationName: String, @@ -176,8 +189,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC dataSetName: String, dataLayerName: String, listMeshChunksRequest: ListMeshChunksRequest): Fox[WebknossosSegmentInfo] = { - // TODO neue routen bauen und entsprechende datentypen zurückgeben - val meshFilePath = dataBaseDir .resolve(organizationName) @@ -186,18 +197,25 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") + println(s"Reading $meshFilePath") + safeExecute(meshFilePath) { cachedMeshFile => val segmentId = listMeshChunksRequest.segmentId + println(s"Reading $segmentId") + val encoding = cachedMeshFile.reader.string().getAttr("/", "mesh_format") + val lodScaleMultiplier = cachedMeshFile.reader.float64().getAttr("/", "lod_scale_multiplier") + println(s"encoding: $encoding") + val transform = cachedMeshFile.reader.float64().readMatrixBlockWithOffset("/transform", 3, 4, 0, 0) + println(s"transform: ${transform.mkString("Array(", ", ", ")")}") + val (neuroglancerStart, neuroglancerEnd) = getNeuroglancerOffsets(segmentId, cachedMeshFile) - val transform = cachedMeshFile.reader.float32().readMatrixBlockWithOffset("metadata/transform", 4, 3, 0, 0) - val encoding = cachedMeshFile.reader.string().getAttr("/", "metadata/mesh_format") + println(s"from $neuroglancerStart to $neuroglancerEnd") - val lodScaleMultiplier = cachedMeshFile.reader.float32().getAttr("", "metadata/lod_scale_multiplier") val manifest = cachedMeshFile.reader .uint8() - .readArrayBlockWithOffset("neuroglancer", (neuroglancerEnd - neuroglancerStart + 1).toInt, neuroglancerStart) + .readArrayBlockWithOffset("/neuroglancer", (neuroglancerEnd - neuroglancerStart + 1).toInt, neuroglancerStart) val segmentInfo = parseNeuroglancerManifest(manifest) - val meshfile = getFragmentsFromSegmentInfo(segmentInfo, transform, lodScaleMultiplier, neuroglancerStart) + val meshfile = getFragmentsFromSegmentInfo(segmentInfo, lodScaleMultiplier, neuroglancerStart) meshfile .map(meshInfo => WebknossosSegmentInfo(transform = transform, meshFormat = encoding, chunks = meshInfo)) .toFox @@ -205,27 +223,22 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC } private def getFragmentsFromSegmentInfo(segmentInfo: NeuroglancerSegmentInfo, - transform: Array[Array[Float]], - lodScaleMultiplier: Float, + lodScaleMultiplier: Double, neuroglancerOffsetStart: Long): Option[MeshSegmentInfo] = { val totalMeshSize = segmentInfo.fragmentOffsets.map(_.sum).sum val meshByteStartOffset = neuroglancerOffsetStart - totalMeshSize val fragmentByteOffsets = segmentInfo.fragmentOffsets.map(_.scanLeft(0)(_ + _)) // This builds a cumulative sum - def computeGlobalPositionAndOffset(lod: Int, currentFragment: Int): Option[MeshFragment] = { + def computeGlobalPositionAndOffset(lod: Int, currentFragment: Int): MeshFragment = { val globalPosition = segmentInfo.gridOrigin + segmentInfo .fragmentPositions(lod)(currentFragment) .toVec3Float * segmentInfo.chunkShape * segmentInfo.lodScales(lod) * lodScaleMultiplier - // todo this is still the wrong way around - val transformedPosition = globalPosition.matmul(transform) - transformedPosition.map( - position => MeshFragment( - position = position, + position = globalPosition, // This position is in Voxel Space byteOffset = meshByteStartOffset.toInt + fragmentByteOffsets(lod)(currentFragment), byteSize = segmentInfo.fragmentOffsets(lod)(currentFragment), - )) + ) } val lods = for (lod <- 0 until segmentInfo.numLods) yield lod @@ -235,20 +248,16 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC yield (lod, currentFragment) val fragments = lods.map(lod => fragmentNums(lod).map(x => computeGlobalPositionAndOffset(x._1, x._2)).toList) - if (fragments.contains(None)) - None - else { - val meshfileLods = lods - .map( - lod => - MeshLodInfo(scale = segmentInfo.lodScales(lod).toInt, - vertexOffset = segmentInfo.vertexOffsets(lod), - chunkShape = segmentInfo.chunkShape, - fragments = fragments(lod).flatten)) - .toList - Some( - MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods)) - } + val meshfileLods = lods + .map( + lod => + MeshLodInfo(scale = segmentInfo.lodScales(lod).toInt, + vertexOffset = segmentInfo.vertexOffsets(lod), + chunkShape = segmentInfo.chunkShape, + fragments = fragments(lod))) + .toList + Some( + MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods)) } private def parseNeuroglancerManifest(manifest: Array[Byte]): NeuroglancerSegmentInfo = { // All Ints here should be UInt32 per spec. @@ -306,8 +315,11 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC } private def getNeuroglancerOffsets(segmentId: Long, cachedMeshFile: CachedHdf5File): (Long, Long) = { - val nBuckets = cachedMeshFile.reader.uint64().getAttr("/", "metadata/n_buckets") - val hashName = cachedMeshFile.reader.string().getAttr("/", "metadata/hash_function") + val nBuckets = cachedMeshFile.reader.uint64().getAttr("/", "n_buckets") + val hashName = cachedMeshFile.reader.string().getAttr("/", "hash_function") + + println(s"nBuckets: $nBuckets") + println(s"hashName: $hashName") val bucketIndex = getHashFunction(hashName)(segmentId) % nBuckets val cappedBucketIndex = bucketIndex.toInt @@ -359,7 +371,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(s"${meshChunkDataRequest.meshFile}.$meshFileExtension") safeExecute(meshFilePath) { cachedMeshFile => - val meshFormat = cachedMeshFile.reader.string().getAttr("/", "metadata/mesh_format") + val meshFormat = cachedMeshFile.reader.string().getAttr("/", "mesh_format") val data = cachedMeshFile.reader .uint8() diff --git a/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes b/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes index 7ef4caa6bd7..b92f4d95825 100644 --- a/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes +++ b/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes @@ -60,7 +60,7 @@ POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/mes POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/v1/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentV1(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/v1/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkV1(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) - +GET /datasets/dummyDraco @com.scalableminds.webknossos.datastore.controllers.DataSourceController.dummyDracoFile() # Connectome files GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listConnectomeFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) From 56cd3f7c772be2c8bff890dc94f195d6dc3334fb Mon Sep 17 00:00:00 2001 From: leowe Date: Tue, 27 Sep 2022 14:12:57 +0200 Subject: [PATCH 07/63] remove draco file from binaryData --- binaryData/draco_file.bin | Bin 251 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 binaryData/draco_file.bin diff --git a/binaryData/draco_file.bin b/binaryData/draco_file.bin deleted file mode 100644 index 96ec360ddbfef72332e9fde2f3bd34fa2ca6357d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 251 zcmV)B3}%UkOd?Jr8rgk~f~*4|bojJJO?w9xUW0^R z3@`#C*tQ#hN(6s|!VKUE+ChjrKt6sRiT;SkGr%vQ0OT2P z4>WH*xZ&j)8oLrO(-Mej0RZS9k%A#c*ra}V%gCn5Vd+#I4)}zbCxAUIlr|Fp5#K=r zD)LgeR*z(>1BSso@%I7L1|#h_yQgXe000000RIC30I#64LN8J}LwdNFLtsh!K?(<^ BTtNT; From 786129c912cd7afc4826af4ffaa1f29c19c911b1 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 27 Sep 2022 15:54:26 +0200 Subject: [PATCH 08/63] add dummy loading of draco encoded file --- frontend/javascripts/admin/admin_rest_api.ts | 17 ++++++++ .../oxalis/model/sagas/isosurface_saga.ts | 40 +++++++++++++++++++ .../oxalis/view/action_bar_view.tsx | 5 +++ package.json | 1 + yarn.lock | 5 +++ 5 files changed, 68 insertions(+) diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts index d1949635572..45c4016a254 100644 --- a/frontend/javascripts/admin/admin_rest_api.ts +++ b/frontend/javascripts/admin/admin_rest_api.ts @@ -2165,6 +2165,23 @@ export function getMeshfileChunkData( }); } +export function getDummyDraco(): Promise { + return doWithToken(async (token) => { + const data = await Request.receiveArraybuffer(`/assets/draco_file.bin`, { + // const data = await Request.receiveArraybuffer(`/data/datasets/dummyDraco?token=${token}`, { + useWebworkerForArrayBuffer: false, + }); + + console.log("works", data, new Uint8Array(data)); + + const data2 = await Request.receiveArraybuffer(`/data/datasets/dummyDraco?token=${token}`, { + useWebworkerForArrayBuffer: false, + }); + console.log("doesnt work", data2, new Uint8Array(data2)); + return data2; + }); +} + // ### Connectomes export function getConnectomeFilesForDatasetLayer( dataStoreUrl: string, diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 9ed29a8afd7..f000cf2081d 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -4,6 +4,8 @@ import { V3 } from "libs/mjs"; import { sleep } from "libs/utils"; import ErrorHandling from "libs/error_handling"; import type { APIDataLayer } from "types/api_flow_types"; +import "libs/DRACOLoader.js"; + import { ResolutionInfo, getResolutionInfo, @@ -42,6 +44,7 @@ import { sendAnalyticsEvent, getMeshfileChunksForSegment, getMeshfileChunkData, + getDummyDraco, } from "admin/admin_rest_api"; import { getFlooredPosition } from "oxalis/model/accessors/flycam_accessor"; import { setImportingMeshStateAction } from "oxalis/model/actions/ui_actions"; @@ -60,6 +63,8 @@ import messages from "messages"; import processTaskWithPool from "libs/task_pool"; import { getBaseSegmentationName } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { UpdateSegmentAction } from "../actions/volumetracing_actions"; +import * as THREE from "three"; + const MAX_RETRY_COUNT = 5; const RETRY_WAIT_TIME = 5000; const MESH_CHUNK_THROTTLE_DELAY = 500; @@ -602,6 +607,40 @@ function* loadPrecomputedMeshForSegmentId( yield* put(finishedLoadingIsosurfaceAction(layerName, id)); } +function* addDummyDraco() { + const buffer = yield* call(getDummyDraco); + console.log("buffer", buffer); + + const loader = new THREE.DRACOLoader(); + console.log("THREE.DRACOLoader", loader); + + loader.setDecoderPath( + "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/", + ); + loader.setDecoderConfig({ type: "js" }); + + const sceneController = yield* call(getSceneController); + + // const url = "/data/datasets/dummyDraco"; + const url = "/assets/draco_file.bin"; + // const url = "http://localhost:9000/assets/bunny.drc"; + // loader.load(url, (geometry) => { + loader.decodeDracoFile(buffer, (geometry) => { + // geometry.computeVertexNormals(); + + // todo: use correct id + sceneController.addIsosurfaceFromGeometry(geometry, 2347819234); + + // Release decoder resources. + loader.dispose(); + }); + + // const decoderModule = yield* call(() => draco3d.createDecoderModule({})); + // let decoder = new decoderModule.Decoder(); + // console.log("decoder"); + // const geometry = yield* call(parseStlBuffer, stlData); +} + /* * * Ad Hoc and Precomputed Meshes @@ -689,4 +728,5 @@ export default function* isosurfaceSaga(): Saga { yield* takeEvery("UPDATE_ISOSURFACE_VISIBILITY", handleIsosurfaceVisibilityChange); yield* takeEvery(["START_EDITING", "COPY_SEGMENTATION_LAYER"], markEditedCellAsDirty); yield* takeEvery(["UPDATE_SEGMENT"], handleIsosurfaceColorChange); + yield* takeEvery(["ADD_DUMMY_DRACO"], addDummyDraco); } diff --git a/frontend/javascripts/oxalis/view/action_bar_view.tsx b/frontend/javascripts/oxalis/view/action_bar_view.tsx index 7370f1d9794..bdcc58c0de4 100644 --- a/frontend/javascripts/oxalis/view/action_bar_view.tsx +++ b/frontend/javascripts/oxalis/view/action_bar_view.tsx @@ -28,6 +28,7 @@ import { getVisibleSegmentationLayer, } from "oxalis/model/accessors/dataset_accessor"; import { AsyncButton } from "components/async_clickables"; +import ButtonComponent from "./components/button_component"; const VersionRestoreWarning = ( { {!isReadOnly && constants.MODES_PLANE.indexOf(viewMode) > -1 ? : null} {isArbitrarySupported && !is2d ? : null} {isViewMode ? this.renderStartTracingButton() : null} + + Store.dispatch({ type: "ADD_DUMMY_DRACO" })}> + ADD_DUMMY_DRACO + Date: Tue, 27 Sep 2022 15:54:39 +0200 Subject: [PATCH 09/63] add dummy draco file --- public/draco_file.bin | Bin 0 -> 251 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/draco_file.bin diff --git a/public/draco_file.bin b/public/draco_file.bin new file mode 100644 index 0000000000000000000000000000000000000000..96ec360ddbfef72332e9fde2f3bd34fa2ca6357d GIT binary patch literal 251 zcmV)B3}%UkOd?Jr8rgk~f~*4|bojJJO?w9xUW0^R z3@`#C*tQ#hN(6s|!VKUE+ChjrKt6sRiT;SkGr%vQ0OT2P z4>WH*xZ&j)8oLrO(-Mej0RZS9k%A#c*ra}V%gCn5Vd+#I4)}zbCxAUIlr|Fp5#K=r zD)LgeR*z(>1BSso@%I7L1|#h_yQgXe000000RIC30I#64LN8J}LwdNFLtsh!K?(<^ BTtNT; literal 0 HcmV?d00001 From 220a97a4a759ff3dd0347661eba861089d0d1993 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 27 Sep 2022 16:13:40 +0200 Subject: [PATCH 10/63] add draco loader --- frontend/javascripts/libs/DRACOLoader.js | 467 +++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 frontend/javascripts/libs/DRACOLoader.js diff --git a/frontend/javascripts/libs/DRACOLoader.js b/frontend/javascripts/libs/DRACOLoader.js new file mode 100644 index 00000000000..c04219bf122 --- /dev/null +++ b/frontend/javascripts/libs/DRACOLoader.js @@ -0,0 +1,467 @@ +import * as THREE from "three"; + +(function () { + const _taskCache = new WeakMap(); + + class DRACOLoader extends THREE.Loader { + constructor(manager) { + super(manager); + this.decoderPath = ""; + this.decoderConfig = {}; + this.decoderBinary = null; + this.decoderPending = null; + this.workerLimit = 4; + this.workerPool = []; + this.workerNextTaskID = 1; + this.workerSourceURL = ""; + this.defaultAttributeIDs = { + position: "POSITION", + normal: "NORMAL", + color: "COLOR", + uv: "TEX_COORD", + }; + this.defaultAttributeTypes = { + position: "Float32Array", + normal: "Float32Array", + color: "Float32Array", + uv: "Float32Array", + }; + } + + setDecoderPath(path) { + this.decoderPath = path; + return this; + } + + setDecoderConfig(config) { + this.decoderConfig = config; + return this; + } + + setWorkerLimit(workerLimit) { + this.workerLimit = workerLimit; + return this; + } + + load(url, onLoad, onProgress, onError) { + const loader = new THREE.FileLoader(this.manager); + loader.setPath(this.path); + loader.setResponseType("arraybuffer"); + loader.setRequestHeader(this.requestHeader); + loader.setWithCredentials(this.withCredentials); + loader.load( + url, + (buffer) => { + this.decodeDracoFile(buffer, onLoad).catch(onError); + }, + onProgress, + onError, + ); + } + + decodeDracoFile(buffer, callback, attributeIDs, attributeTypes) { + const taskConfig = { + attributeIDs: attributeIDs || this.defaultAttributeIDs, + attributeTypes: attributeTypes || this.defaultAttributeTypes, + useUniqueIDs: !!attributeIDs, + }; + return this.decodeGeometry(buffer, taskConfig).then(callback); + } + + decodeGeometry(buffer, taskConfig) { + const taskKey = JSON.stringify(taskConfig); // Check for an existing task using this buffer. A transferred buffer cannot be transferred + // again from this thread. + + if (_taskCache.has(buffer)) { + const cachedTask = _taskCache.get(buffer); + + if (cachedTask.key === taskKey) { + return cachedTask.promise; + } else if (buffer.byteLength === 0) { + // Technically, it would be possible to wait for the previous task to complete, + // transfer the buffer back, and decode again with the second configuration. That + // is complex, and I don't know of any reason to decode a Draco buffer twice in + // different ways, so this is left unimplemented. + throw new Error( + "THREE.DRACOLoader: Unable to re-decode a buffer with different " + + "settings. Buffer has already been transferred.", + ); + } + } // + + let worker; + const taskID = this.workerNextTaskID++; + const taskCost = buffer.byteLength; // Obtain a worker and assign a task, and construct a geometry instance + // when the task completes. + + const geometryPending = this._getWorker(taskID, taskCost) + .then((_worker) => { + worker = _worker; + return new Promise((resolve, reject) => { + worker._callbacks[taskID] = { + resolve, + reject, + }; + worker.postMessage( + { + type: "decode", + id: taskID, + taskConfig, + buffer, + }, + [buffer], + ); // this.debug(); + }); + }) + .then((message) => this._createGeometry(message.geometry)); // Remove task from the task list. + // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416) + + geometryPending + .catch(() => true) + .then(() => { + if (worker && taskID) { + this._releaseTask(worker, taskID); // this.debug(); + } + }); // Cache the task result. + + _taskCache.set(buffer, { + key: taskKey, + promise: geometryPending, + }); + + return geometryPending; + } + + _createGeometry(geometryData) { + const geometry = new THREE.BufferGeometry(); + + if (geometryData.index) { + geometry.setIndex(new THREE.BufferAttribute(geometryData.index.array, 1)); + } + + for (let i = 0; i < geometryData.attributes.length; i++) { + const attribute = geometryData.attributes[i]; + const name = attribute.name; + const array = attribute.array; + const itemSize = attribute.itemSize; + geometry.setAttribute(name, new THREE.BufferAttribute(array, itemSize)); + } + + return geometry; + } + + _loadLibrary(url, responseType) { + const loader = new THREE.FileLoader(this.manager); + loader.setPath(this.decoderPath); + loader.setResponseType(responseType); + loader.setWithCredentials(this.withCredentials); + return new Promise((resolve, reject) => { + loader.load(url, resolve, undefined, reject); + }); + } + + preload() { + this._initDecoder(); + + return this; + } + + _initDecoder() { + if (this.decoderPending) return this.decoderPending; + const useJS = typeof WebAssembly !== "object" || this.decoderConfig.type === "js"; + const librariesPending = []; + + if (useJS) { + librariesPending.push(this._loadLibrary("draco_decoder.js", "text")); + } else { + librariesPending.push(this._loadLibrary("draco_wasm_wrapper.js", "text")); + librariesPending.push(this._loadLibrary("draco_decoder.wasm", "arraybuffer")); + } + + this.decoderPending = Promise.all(librariesPending).then((libraries) => { + const jsContent = libraries[0]; + + if (!useJS) { + this.decoderConfig.wasmBinary = libraries[1]; + } + + const fn = DRACOWorker.toString(); + const body = [ + "/* draco decoder */", + jsContent, + "", + "/* worker */", + fn.substring(fn.indexOf("{") + 1, fn.lastIndexOf("}")), + ].join("\n"); + this.workerSourceURL = URL.createObjectURL(new Blob([body])); + }); + return this.decoderPending; + } + + _getWorker(taskID, taskCost) { + return this._initDecoder().then(() => { + if (this.workerPool.length < this.workerLimit) { + const worker = new Worker(this.workerSourceURL); + worker._callbacks = {}; + worker._taskCosts = {}; + worker._taskLoad = 0; + worker.postMessage({ + type: "init", + decoderConfig: this.decoderConfig, + }); + + worker.onmessage = function (e) { + const message = e.data; + + switch (message.type) { + case "decode": + worker._callbacks[message.id].resolve(message); + + break; + + case "error": + worker._callbacks[message.id].reject(message); + + break; + + default: + console.error('THREE.DRACOLoader: Unexpected message, "' + message.type + '"'); + } + }; + + this.workerPool.push(worker); + } else { + this.workerPool.sort(function (a, b) { + return a._taskLoad > b._taskLoad ? -1 : 1; + }); + } + + const worker = this.workerPool[this.workerPool.length - 1]; + worker._taskCosts[taskID] = taskCost; + worker._taskLoad += taskCost; + return worker; + }); + } + + _releaseTask(worker, taskID) { + worker._taskLoad -= worker._taskCosts[taskID]; + delete worker._callbacks[taskID]; + delete worker._taskCosts[taskID]; + } + + debug() { + console.log( + "Task load: ", + this.workerPool.map((worker) => worker._taskLoad), + ); + } + + dispose() { + for (let i = 0; i < this.workerPool.length; ++i) { + this.workerPool[i].terminate(); + } + + this.workerPool.length = 0; + return this; + } + } + /* WEB WORKER */ + + function DRACOWorker() { + let decoderConfig; + let decoderPending; + + onmessage = function (e) { + const message = e.data; + + switch (message.type) { + case "init": + decoderConfig = message.decoderConfig; + decoderPending = new Promise(function ( + resolve, + /*, reject*/ + ) { + decoderConfig.onModuleLoaded = function (draco) { + // Module is Promise-like. Wrap before resolving to avoid loop. + resolve({ + draco: draco, + }); + }; + + DracoDecoderModule(decoderConfig); // eslint-disable-line no-undef + }); + break; + + case "decode": + const buffer = message.buffer; + const taskConfig = message.taskConfig; + decoderPending.then((module) => { + const draco = module.draco; + const decoder = new draco.Decoder(); + const decoderBuffer = new draco.DecoderBuffer(); + decoderBuffer.Init(new Int8Array(buffer), buffer.byteLength); + + try { + const geometry = decodeGeometry(draco, decoder, decoderBuffer, taskConfig); + const buffers = geometry.attributes.map((attr) => attr.array.buffer); + if (geometry.index) buffers.push(geometry.index.array.buffer); + self.postMessage( + { + type: "decode", + id: message.id, + geometry, + }, + buffers, + ); + } catch (error) { + console.error(error); + self.postMessage({ + type: "error", + id: message.id, + error: error.message, + }); + } finally { + draco.destroy(decoderBuffer); + draco.destroy(decoder); + } + }); + break; + } + }; + + function decodeGeometry(draco, decoder, decoderBuffer, taskConfig) { + const attributeIDs = taskConfig.attributeIDs; + const attributeTypes = taskConfig.attributeTypes; + let dracoGeometry; + let decodingStatus; + const geometryType = decoder.GetEncodedGeometryType(decoderBuffer); + + if (geometryType === draco.TRIANGULAR_MESH) { + dracoGeometry = new draco.Mesh(); + decodingStatus = decoder.DecodeBufferToMesh(decoderBuffer, dracoGeometry); + } else if (geometryType === draco.POINT_CLOUD) { + dracoGeometry = new draco.PointCloud(); + decodingStatus = decoder.DecodeBufferToPointCloud(decoderBuffer, dracoGeometry); + } else { + throw new Error("THREE.DRACOLoader: Unexpected geometry type."); + } + + if (!decodingStatus.ok() || dracoGeometry.ptr === 0) { + throw new Error("THREE.DRACOLoader: Decoding failed: " + decodingStatus.error_msg()); + } + + const geometry = { + index: null, + attributes: [], + }; // Gather all vertex attributes. + + for (const attributeName in attributeIDs) { + const attributeType = self[attributeTypes[attributeName]]; + let attribute; + let attributeID; // A Draco file may be created with default vertex attributes, whose attribute IDs + // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively, + // a Draco file may contain a custom set of attributes, identified by known unique + // IDs. glTF files always do the latter, and `.drc` files typically do the former. + + if (taskConfig.useUniqueIDs) { + attributeID = attributeIDs[attributeName]; + attribute = decoder.GetAttributeByUniqueId(dracoGeometry, attributeID); + } else { + attributeID = decoder.GetAttributeId(dracoGeometry, draco[attributeIDs[attributeName]]); + if (attributeID === -1) continue; + attribute = decoder.GetAttribute(dracoGeometry, attributeID); + } + + geometry.attributes.push( + decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute), + ); + } // Add index. + + if (geometryType === draco.TRIANGULAR_MESH) { + geometry.index = decodeIndex(draco, decoder, dracoGeometry); + } + + draco.destroy(dracoGeometry); + return geometry; + } + + function decodeIndex(draco, decoder, dracoGeometry) { + const numFaces = dracoGeometry.num_faces(); + const numIndices = numFaces * 3; + const byteLength = numIndices * 4; + + const ptr = draco._malloc(byteLength); + + decoder.GetTrianglesUInt32Array(dracoGeometry, byteLength, ptr); + const index = new Uint32Array(draco.HEAPF32.buffer, ptr, numIndices).slice(); + + draco._free(ptr); + + return { + array: index, + itemSize: 1, + }; + } + + function decodeAttribute( + draco, + decoder, + dracoGeometry, + attributeName, + attributeType, + attribute, + ) { + const numComponents = attribute.num_components(); + const numPoints = dracoGeometry.num_points(); + const numValues = numPoints * numComponents; + const byteLength = numValues * attributeType.BYTES_PER_ELEMENT; + const dataType = getDracoDataType(draco, attributeType); + + const ptr = draco._malloc(byteLength); + + decoder.GetAttributeDataArrayForAllPoints( + dracoGeometry, + attribute, + dataType, + byteLength, + ptr, + ); + const array = new attributeType(draco.HEAPF32.buffer, ptr, numValues).slice(); + + draco._free(ptr); + + return { + name: attributeName, + array: array, + itemSize: numComponents, + }; + } + + function getDracoDataType(draco, attributeType) { + switch (attributeType) { + case Float32Array: + return draco.DT_FLOAT32; + + case Int8Array: + return draco.DT_INT8; + + case Int16Array: + return draco.DT_INT16; + + case Int32Array: + return draco.DT_INT32; + + case Uint8Array: + return draco.DT_UINT8; + + case Uint16Array: + return draco.DT_UINT16; + + case Uint32Array: + return draco.DT_UINT32; + } + } + } + + THREE.DRACOLoader = DRACOLoader; +})(); From b931cda0ce2557ea277ab6b2964725468d647419 Mon Sep 17 00:00:00 2001 From: leowe Date: Tue, 27 Sep 2022 16:14:56 +0200 Subject: [PATCH 11/63] change response from dummyDracoFile --- .../controllers/DataSourceController.scala | 2 +- .../datastore/services/MeshFileService.scala | 81 ++++++++++++------- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala index 541fd47c1b9..e08e20837f6 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala @@ -403,7 +403,7 @@ Expects: def dummyDracoFile(): Action[AnyContent] = Action.async { implicit request => for { draco <- meshFileService.readDummyDraco() - } yield Ok(Json.toJson(draco)) + } yield Ok(draco) } @ApiOperation(hidden = true, value = "") diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 2f6a6fcb5f6..94560b1dd99 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -18,6 +18,7 @@ import java.nio.ByteBuffer import java.nio.file.{Files, Path, Paths} import javax.inject.Inject import scala.collection.JavaConverters._ +import scala.collection.mutable.ListBuffer import scala.concurrent.ExecutionContext import scala.util.Using @@ -68,8 +69,8 @@ case class NeuroglancerSegmentInfo(chunkShape: Vec3Float, lodScales: Array[Float], vertexOffsets: Array[Vec3Float], numFragmentsPerLod: Array[Int], - fragmentPositions: Array[Array[Vec3Int]], - fragmentOffsets: Array[Array[Int]]) + fragmentPositions: List[List[Vec3Int]], + fragmentOffsets: List[List[Int]]) case class MeshFragment(position: Vec3Float, byteOffset: Int, byteSize: Int) @@ -129,7 +130,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC mappingVersionForMeshFile(meshFilePath) } - val mappingNameFoxes = (meshFileNames, meshFileVersions).zipped.map { (fileName, fileVersion) => + val mappingNameFoxes = (meshFileNames, meshFileVersions).zipped.map { (fileName, fileVersion) => val meshFilePath = layerDir.resolve(meshesDir).resolve(s"$fileName.$meshFileExtension") mappingNameForMeshFile(meshFilePath, fileVersion) } @@ -146,7 +147,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC Note that null is a valid value here for once. Meshfiles with no information about the meshFilePath will return Fox.empty, while meshfiles with one marked as empty, will return Fox.successful(null) */ - def mappingNameForMeshFile(meshFilePath: Path, meshFileVersion: Long): Fox[String] = { + def mappingNameForMeshFile(meshFilePath: Path, meshFileVersion: Long): Fox[String] = if (meshFileVersion == 0) { safeExecute(meshFilePath) { cachedMeshFile => cachedMeshFile.reader.string().getAttr("/", "metadata/mapping_name") @@ -156,7 +157,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC cachedMeshFile.reader.string().getAttr("/", "mapping_name") } ?~> "mesh.file.readEncoding.failed" } - } def mappingVersionForMeshFile(meshFilePath: Path): Long = safeExecuteBox(meshFilePath) { cachedMeshFile => @@ -205,7 +205,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val encoding = cachedMeshFile.reader.string().getAttr("/", "mesh_format") val lodScaleMultiplier = cachedMeshFile.reader.float64().getAttr("/", "lod_scale_multiplier") println(s"encoding: $encoding") - val transform = cachedMeshFile.reader.float64().readMatrixBlockWithOffset("/transform", 3, 4, 0, 0) + val transform = cachedMeshFile.reader.float64().getMatrixAttr("/", "transform") println(s"transform: ${transform.mkString("Array(", ", ", ")")}") val (neuroglancerStart, neuroglancerEnd) = getNeuroglancerOffsets(segmentId, cachedMeshFile) @@ -213,7 +213,8 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val manifest = cachedMeshFile.reader .uint8() - .readArrayBlockWithOffset("/neuroglancer", (neuroglancerEnd - neuroglancerStart + 1).toInt, neuroglancerStart) + .readArrayBlockWithOffset("/neuroglancer", (neuroglancerEnd - neuroglancerStart).toInt, neuroglancerStart) + println(s"""manifest: ${manifest.mkString("Array(", ", ", ")")}""") val segmentInfo = parseNeuroglancerManifest(manifest) val meshfile = getFragmentsFromSegmentInfo(segmentInfo, lodScaleMultiplier, neuroglancerStart) meshfile @@ -229,16 +230,19 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val meshByteStartOffset = neuroglancerOffsetStart - totalMeshSize val fragmentByteOffsets = segmentInfo.fragmentOffsets.map(_.scanLeft(0)(_ + _)) // This builds a cumulative sum + println(s"totalMeshSize: $totalMeshSize") + println(s"meshByteStartOffset: $meshByteStartOffset") + println(s"fragmentByteOffsets: ${fragmentByteOffsets.mkString("Array(", ", ", ")")}") def computeGlobalPositionAndOffset(lod: Int, currentFragment: Int): MeshFragment = { val globalPosition = segmentInfo.gridOrigin + segmentInfo .fragmentPositions(lod)(currentFragment) .toVec3Float * segmentInfo.chunkShape * segmentInfo.lodScales(lod) * lodScaleMultiplier - MeshFragment( - position = globalPosition, // This position is in Voxel Space - byteOffset = meshByteStartOffset.toInt + fragmentByteOffsets(lod)(currentFragment), - byteSize = segmentInfo.fragmentOffsets(lod)(currentFragment), - ) + MeshFragment( + position = globalPosition, // This position is in Voxel Space + byteOffset = meshByteStartOffset.toInt + fragmentByteOffsets(lod)(currentFragment), + byteSize = segmentInfo.fragmentOffsets(lod)(currentFragment), + ) } val lods = for (lod <- 0 until segmentInfo.numLods) yield lod @@ -256,8 +260,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC chunkShape = segmentInfo.chunkShape, fragments = fragments(lod))) .toList - Some( - MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods)) + Some(MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods)) } private def parseNeuroglancerManifest(manifest: Array[Byte]): NeuroglancerSegmentInfo = { // All Ints here should be UInt32 per spec. @@ -272,6 +275,10 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val numLods = dis.readInt + println(s"chunkShape: $chunkShape") + println(s"gridOrigin: $gridOrigin") + println(s"numLods: $numLods") + val lodScales = new Array[Float](numLods) for (d <- 0 until numLods) { lodScales(d) = dis.readFloat @@ -282,26 +289,35 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC vertexOffsets(d) = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) } + println(s"lodScales: ${lodScales.mkString("Array(", ", ", ")")}") + println(s"vertexOffsets: ${lodScales.mkString("Array(", ", ", ")")}") + val numFragmentsPerLod = new Array[Int](numLods) for (lod <- 0 until numLods) { numFragmentsPerLod(lod) = dis.readInt() } - val fragmentPositions = new Array[Array[Array[Int]]](numLods) - val fragmentPositionsVec3 = new Array[Array[Vec3Int]](numLods) - val fragmentSizes = new Array[Array[Int]](numLods) + println(s"numFragmentsPerLod: ${numFragmentsPerLod.mkString("Array(", ", ", ")")}") + // TODO what if there are no fragments? + val fragmentPositionsList = new ListBuffer[List[Vec3Int]] + val fragmentSizes = new ListBuffer[List[Int]] for (lod <- 0 until numLods) { - for (row <- 0 until 3) { - for (col <- 0 until numFragmentsPerLod(lod)) { - fragmentPositions(lod)(row)(col) = dis.readInt + val currentFragmentPositions = (ListBuffer[Int](), ListBuffer[Int](), ListBuffer[Int]()) + for (row <- 0 until 3; _ <- 0 until numFragmentsPerLod(lod)) { + row match { + case 0 => currentFragmentPositions._1.append(dis.readInt) + case 1 => currentFragmentPositions._2.append(dis.readInt) + case 2 => currentFragmentPositions._3.append(dis.readInt) } } - fragmentPositionsVec3(lod) = fragmentPositions(lod).transpose.map(xs => Vec3Int(xs(0), xs(1), xs(2))) + fragmentPositionsList.append(currentFragmentPositions.zipped.map(Vec3Int(_, _, _)).toList) - for (fragmentNum <- 0 until numFragmentsPerLod(lod)) { - fragmentSizes(lod)(fragmentNum) = dis.readInt + val currentFragmentSizes = ListBuffer[Int]() + for (_ <- 0 until numFragmentsPerLod(lod)) { + currentFragmentSizes.append(dis.readInt) } + fragmentSizes.append(currentFragmentSizes.toList) } NeuroglancerSegmentInfo(chunkShape, @@ -310,8 +326,8 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC lodScales, vertexOffsets, numFragmentsPerLod, - fragmentPositionsVec3, - fragmentSizes) + fragmentPositionsList.toList, + fragmentSizes.toList) } private def getNeuroglancerOffsets(segmentId: Long, cachedMeshFile: CachedHdf5File): (Long, Long) = { @@ -324,17 +340,28 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val bucketIndex = getHashFunction(hashName)(segmentId) % nBuckets val cappedBucketIndex = bucketIndex.toInt val bucketOffsets = cachedMeshFile.reader.uint64().readArrayBlockWithOffset("bucket_offsets", 2, bucketIndex) - val bucketStart = bucketOffsets(cappedBucketIndex) + println(s"bucketOffsets: ${bucketOffsets.mkString("Array(", ", ", ")")}") + println(s"cappedBucketIndex: $cappedBucketIndex") + val bucketStart = bucketOffsets(0) val cappedBucketStart = bucketStart.toInt - val bucketEnd = bucketOffsets(cappedBucketIndex + 1) + val bucketEnd = bucketOffsets(1) val cappedBucketEnd = bucketEnd.toInt + + println(s"bucketStart: ${cappedBucketStart}, bucketEnd: ${cappedBucketEnd}") val buckets = cachedMeshFile.reader .uint64() .readMatrixBlockWithOffset("buckets", cappedBucketEnd - cappedBucketStart + 1, 3, bucketStart, 0) + println(s"buckets: ${buckets.mkString("Array(", ", ", ")")}") + println(s"buckets0: ${buckets(0).mkString("Array(", ", ", ")")}") + println(s"buckets1: ${buckets(1).mkString("Array(", ", ", ")")}") + println(s"buckets2: ${buckets(2).mkString("Array(", ", ", ")")}") + // TODO what if you don't find segment val bucketLocalOffset = buckets.map(_(0)).indexOf(segmentId) val neuroglancerStart = buckets(bucketLocalOffset)(1) val neuroglancerEnd = buckets(bucketLocalOffset)(2) + + println(s"neuroglancerStart: $neuroglancerStart, neurglancerEnd: $neuroglancerEnd") (neuroglancerStart, neuroglancerEnd) } From e214ee0a66a9c9893e5f5746791bd1c083268a04 Mon Sep 17 00:00:00 2001 From: leowe Date: Tue, 27 Sep 2022 16:52:23 +0200 Subject: [PATCH 12/63] remove println --- .../datastore/services/MeshFileService.scala | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 94560b1dd99..c50f4d9a91d 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -201,20 +201,15 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC safeExecute(meshFilePath) { cachedMeshFile => val segmentId = listMeshChunksRequest.segmentId - println(s"Reading $segmentId") val encoding = cachedMeshFile.reader.string().getAttr("/", "mesh_format") val lodScaleMultiplier = cachedMeshFile.reader.float64().getAttr("/", "lod_scale_multiplier") - println(s"encoding: $encoding") val transform = cachedMeshFile.reader.float64().getMatrixAttr("/", "transform") - println(s"transform: ${transform.mkString("Array(", ", ", ")")}") val (neuroglancerStart, neuroglancerEnd) = getNeuroglancerOffsets(segmentId, cachedMeshFile) - println(s"from $neuroglancerStart to $neuroglancerEnd") val manifest = cachedMeshFile.reader .uint8() .readArrayBlockWithOffset("/neuroglancer", (neuroglancerEnd - neuroglancerStart).toInt, neuroglancerStart) - println(s"""manifest: ${manifest.mkString("Array(", ", ", ")")}""") val segmentInfo = parseNeuroglancerManifest(manifest) val meshfile = getFragmentsFromSegmentInfo(segmentInfo, lodScaleMultiplier, neuroglancerStart) meshfile @@ -230,9 +225,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val meshByteStartOffset = neuroglancerOffsetStart - totalMeshSize val fragmentByteOffsets = segmentInfo.fragmentOffsets.map(_.scanLeft(0)(_ + _)) // This builds a cumulative sum - println(s"totalMeshSize: $totalMeshSize") - println(s"meshByteStartOffset: $meshByteStartOffset") - println(s"fragmentByteOffsets: ${fragmentByteOffsets.mkString("Array(", ", ", ")")}") def computeGlobalPositionAndOffset(lod: Int, currentFragment: Int): MeshFragment = { val globalPosition = segmentInfo.gridOrigin + segmentInfo .fragmentPositions(lod)(currentFragment) @@ -275,10 +267,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val numLods = dis.readInt - println(s"chunkShape: $chunkShape") - println(s"gridOrigin: $gridOrigin") - println(s"numLods: $numLods") - val lodScales = new Array[Float](numLods) for (d <- 0 until numLods) { lodScales(d) = dis.readFloat @@ -289,15 +277,11 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC vertexOffsets(d) = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) } - println(s"lodScales: ${lodScales.mkString("Array(", ", ", ")")}") - println(s"vertexOffsets: ${lodScales.mkString("Array(", ", ", ")")}") - val numFragmentsPerLod = new Array[Int](numLods) for (lod <- 0 until numLods) { numFragmentsPerLod(lod) = dis.readInt() } - println(s"numFragmentsPerLod: ${numFragmentsPerLod.mkString("Array(", ", ", ")")}") // TODO what if there are no fragments? val fragmentPositionsList = new ListBuffer[List[Vec3Int]] val fragmentSizes = new ListBuffer[List[Int]] @@ -334,34 +318,21 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val nBuckets = cachedMeshFile.reader.uint64().getAttr("/", "n_buckets") val hashName = cachedMeshFile.reader.string().getAttr("/", "hash_function") - println(s"nBuckets: $nBuckets") - println(s"hashName: $hashName") - val bucketIndex = getHashFunction(hashName)(segmentId) % nBuckets val cappedBucketIndex = bucketIndex.toInt val bucketOffsets = cachedMeshFile.reader.uint64().readArrayBlockWithOffset("bucket_offsets", 2, bucketIndex) - println(s"bucketOffsets: ${bucketOffsets.mkString("Array(", ", ", ")")}") - println(s"cappedBucketIndex: $cappedBucketIndex") val bucketStart = bucketOffsets(0) - val cappedBucketStart = bucketStart.toInt val bucketEnd = bucketOffsets(1) - val cappedBucketEnd = bucketEnd.toInt - println(s"bucketStart: ${cappedBucketStart}, bucketEnd: ${cappedBucketEnd}") val buckets = cachedMeshFile.reader .uint64() - .readMatrixBlockWithOffset("buckets", cappedBucketEnd - cappedBucketStart + 1, 3, bucketStart, 0) + .readMatrixBlockWithOffset("buckets", (bucketEnd - bucketStart + 1).toInt, 3, bucketStart, 0) - println(s"buckets: ${buckets.mkString("Array(", ", ", ")")}") - println(s"buckets0: ${buckets(0).mkString("Array(", ", ", ")")}") - println(s"buckets1: ${buckets(1).mkString("Array(", ", ", ")")}") - println(s"buckets2: ${buckets(2).mkString("Array(", ", ", ")")}") // TODO what if you don't find segment val bucketLocalOffset = buckets.map(_(0)).indexOf(segmentId) val neuroglancerStart = buckets(bucketLocalOffset)(1) val neuroglancerEnd = buckets(bucketLocalOffset)(2) - println(s"neuroglancerStart: $neuroglancerStart, neurglancerEnd: $neuroglancerEnd") (neuroglancerStart, neuroglancerEnd) } From 610a331e035e38e8730bcf163fe2981ffd421d6b Mon Sep 17 00:00:00 2001 From: leowe Date: Wed, 28 Sep 2022 11:16:48 +0200 Subject: [PATCH 13/63] reformat --- .../controllers/DataSourceController.scala | 32 +++++++++++-------- .../datastore/services/MeshFileService.scala | 5 +-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala index e08e20837f6..c8b6aedf82d 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala @@ -401,9 +401,9 @@ Expects: } def dummyDracoFile(): Action[AnyContent] = Action.async { implicit request => - for { - draco <- meshFileService.readDummyDraco() - } yield Ok(draco) + for { + draco <- meshFileService.readDummyDraco() + } yield Ok(draco) } @ApiOperation(hidden = true, value = "") @@ -430,9 +430,9 @@ Expects: urlOrHeaderToken(token, request)) { for { positions <- meshFileService.listMeshChunksForSegmentV0(organizationName, - dataSetName, - dataLayerName, - request.body) ?~> Messages( + dataSetName, + dataLayerName, + request.body) ?~> Messages( "mesh.file.listChunks.failed", request.body.segmentId.toString, request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST @@ -447,12 +447,12 @@ Expects: dataLayerName: String): Action[ListMeshChunksRequest] = Action.async(validateJson[ListMeshChunksRequest]) { implicit request => accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), - urlOrHeaderToken(token, request)) { + urlOrHeaderToken(token, request)) { for { positions <- meshFileService.listMeshChunksForSegmentV1(organizationName, - dataSetName, - dataLayerName, - request.body) ?~> Messages( + dataSetName, + dataLayerName, + request.body) ?~> Messages( "mesh.file.listChunks.failed", request.body.segmentId.toString, request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST @@ -469,7 +469,10 @@ Expects: accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), urlOrHeaderToken(token, request)) { for { - (data, encoding) <- meshFileService.readMeshChunkV0(organizationName, dataSetName, dataLayerName, request.body) ?~> "mesh.file.loadChunk.failed" + (data, encoding) <- meshFileService.readMeshChunkV0(organizationName, + dataSetName, + dataLayerName, + request.body) ?~> "mesh.file.loadChunk.failed" } yield { if (encoding.contains("gzip")) { Ok(data).withHeaders("Content-Encoding" -> "gzip") @@ -485,9 +488,12 @@ Expects: dataLayerName: String): Action[MeshChunkDataRequestV1] = Action.async(validateJson[MeshChunkDataRequestV1]) { implicit request => accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), - urlOrHeaderToken(token, request)) { + urlOrHeaderToken(token, request)) { for { - (data, encoding) <- meshFileService.readMeshChunkV1(organizationName, dataSetName, dataLayerName, request.body) ?~> "mesh.file.loadChunk.failed" + (data, encoding) <- meshFileService.readMeshChunkV1(organizationName, + dataSetName, + dataLayerName, + request.body) ?~> "mesh.file.loadChunk.failed" } yield { if (encoding.contains("gzip")) { Ok(data).withHeaders("Content-Encoding" -> "gzip") diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index c50f4d9a91d..86545c24255 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -197,8 +197,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") - println(s"Reading $meshFilePath") - safeExecute(meshFilePath) { cachedMeshFile => val segmentId = listMeshChunksRequest.segmentId val encoding = cachedMeshFile.reader.string().getAttr("/", "mesh_format") @@ -319,7 +317,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val hashName = cachedMeshFile.reader.string().getAttr("/", "hash_function") val bucketIndex = getHashFunction(hashName)(segmentId) % nBuckets - val cappedBucketIndex = bucketIndex.toInt val bucketOffsets = cachedMeshFile.reader.uint64().readArrayBlockWithOffset("bucket_offsets", 2, bucketIndex) val bucketStart = bucketOffsets(0) val bucketEnd = bucketOffsets(1) @@ -391,7 +388,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC private def safeExecuteBox[T](filePath: Path)(block: CachedHdf5File => T): Box[T] = for { _ <- if (filePath.toFile.exists()) { - new Full + Full(true) } else { Empty ~> "mesh.file.open.failed" } From ef11d686fd8633d83274a666b618a6a5d7737884 Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 29 Sep 2022 10:52:20 +0200 Subject: [PATCH 14/63] add changelog entry --- CHANGELOG.unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 55dd5449149..7bc9dc15a38 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released [Commits](https://github.com/scalableminds/webknossos/compare/22.10.0...HEAD) ### Added +- Support for a new mesh file format which allows up to billions of meshes. [#6491](https://github.com/scalableminds/webknossos/pull/6491) ### Changed - Creating tasks in bulk now also supports referencing task types by their summary instead of id. [#6486](https://github.com/scalableminds/webknossos/pull/6486) From 877a85e73973adeaa1b8938ac0ba0465803dbd4a Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 29 Sep 2022 13:36:50 +0200 Subject: [PATCH 15/63] add formatVersion als parameter in URL --- .../com/scalableminds/util/tools/Fox.scala | 2 +- .../controllers/DataSourceController.scala | 44 +++--- .../datastore/services/MeshFileService.scala | 66 +++------ .../datastore/storage/Hdf5FileCache.scala | 28 ++++ ....scalableminds.webknossos.datastore.routes | 130 +++++++++--------- 5 files changed, 139 insertions(+), 131 deletions(-) diff --git a/util/src/main/scala/com/scalableminds/util/tools/Fox.scala b/util/src/main/scala/com/scalableminds/util/tools/Fox.scala index f862bf9f285..eb0ef55f6cf 100644 --- a/util/src/main/scala/com/scalableminds/util/tools/Fox.scala +++ b/util/src/main/scala/com/scalableminds/util/tools/Fox.scala @@ -33,7 +33,7 @@ trait FoxImplicits { implicit def try2Fox[T](t: Try[T])(implicit ec: ExecutionContext): Fox[T] = t match { case Success(result) => Fox.successful(result) - case scala.util.Failure(e) => Fox.failure(e.getMessage) + case scala.util.Failure(e) => Fox.failure(s"${e.toString}") } implicit def fox2FutureBox[T](f: Fox[T]): Future[Box[T]] = diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala index c8b6aedf82d..b400eea832b 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala @@ -441,21 +441,23 @@ Expects: } @ApiOperation(hidden = true, value = "") - def listMeshChunksForSegmentV1(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String): Action[ListMeshChunksRequest] = + def listMeshChunksForSegmentForVersion(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String, + formatVersion: Int): Action[ListMeshChunksRequest] = Action.async(validateJson[ListMeshChunksRequest]) { implicit request => accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), urlOrHeaderToken(token, request)) { for { - positions <- meshFileService.listMeshChunksForSegmentV1(organizationName, - dataSetName, - dataLayerName, - request.body) ?~> Messages( - "mesh.file.listChunks.failed", - request.body.segmentId.toString, - request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST + positions <- formatVersion match { + case 3 => + meshFileService.listMeshChunksForSegmentV3(organizationName, dataSetName, dataLayerName, request.body) ?~> Messages( + "mesh.file.listChunks.failed", + request.body.segmentId.toString, + request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST + case _ => Fox.failure("Wrong format version") ~> BAD_REQUEST + } } yield Ok(Json.toJson(positions)) } } @@ -482,18 +484,20 @@ Expects: } @ApiOperation(hidden = true, value = "") - def readMeshChunkV1(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String): Action[MeshChunkDataRequestV1] = - Action.async(validateJson[MeshChunkDataRequestV1]) { implicit request => + def readMeshChunkForVersion(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String, + formatVersion: Int): Action[MeshChunkDataRequestV3] = + Action.async(validateJson[MeshChunkDataRequestV3]) { implicit request => accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), urlOrHeaderToken(token, request)) { for { - (data, encoding) <- meshFileService.readMeshChunkV1(organizationName, - dataSetName, - dataLayerName, - request.body) ?~> "mesh.file.loadChunk.failed" + (data, encoding) <- formatVersion match { + case 3 => + meshFileService.readMeshChunkV3(organizationName, dataSetName, dataLayerName, request.body) ?~> "mesh.file.loadChunk.failed" + case _ => Fox.failure("Wrong format version") ~> BAD_REQUEST + } } yield { if (encoding.contains("gzip")) { Ok(data).withHeaders("Content-Encoding" -> "gzip") diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 86545c24255..5ca3eaf546a 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -5,9 +5,10 @@ import com.scalableminds.util.geometry.{Vec3Float, Vec3Int} import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.DataStoreConfig +import com.scalableminds.webknossos.datastore.storage.CachedHdf5Utils.{safeExecute, safeExecuteBox} import com.scalableminds.webknossos.datastore.storage.{CachedHdf5File, Hdf5FileCache} import com.typesafe.scalalogging.LazyLogging -import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.common.Box import net.liftweb.util.Helpers.tryo import org.apache.commons.codec.digest.MurmurHash3 import org.apache.commons.io.FilenameUtils @@ -20,7 +21,6 @@ import javax.inject.Inject import scala.collection.JavaConverters._ import scala.collection.mutable.ListBuffer import scala.concurrent.ExecutionContext -import scala.util.Using trait GenericJsonFormat[T] {} @@ -39,7 +39,7 @@ case class MeshChunkDataRequestV0( segmentId: Long ) -case class MeshChunkDataRequestV1( +case class MeshChunkDataRequestV3( meshFile: String, fragmentStartOffset: Long, fragmentSize: Int @@ -49,8 +49,8 @@ object MeshChunkDataRequestV0 { implicit val jsonFormat: OFormat[MeshChunkDataRequestV0] = Json.format[MeshChunkDataRequestV0] } -object MeshChunkDataRequestV1 { - implicit val jsonFormat: OFormat[MeshChunkDataRequestV1] = Json.format[MeshChunkDataRequestV1] +object MeshChunkDataRequestV3 { + implicit val jsonFormat: OFormat[MeshChunkDataRequestV3] = Json.format[MeshChunkDataRequestV3] } case class MeshFileInfo( @@ -72,6 +72,7 @@ case class NeuroglancerSegmentInfo(chunkShape: Vec3Float, fragmentPositions: List[List[Vec3Int]], fragmentOffsets: List[List[Int]]) +// TODO position als Vec3Int case class MeshFragment(position: Vec3Float, byteOffset: Int, byteSize: Int) object MeshFragment { @@ -149,17 +150,17 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC */ def mappingNameForMeshFile(meshFilePath: Path, meshFileVersion: Long): Fox[String] = if (meshFileVersion == 0) { - safeExecute(meshFilePath) { cachedMeshFile => + safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => cachedMeshFile.reader.string().getAttr("/", "metadata/mapping_name") } ?~> "mesh.file.readEncoding.failed" } else { - safeExecute(meshFilePath) { cachedMeshFile => + safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => cachedMeshFile.reader.string().getAttr("/", "mapping_name") } ?~> "mesh.file.readEncoding.failed" } def mappingVersionForMeshFile(meshFilePath: Path): Long = - safeExecuteBox(meshFilePath) { cachedMeshFile => + safeExecuteBox(meshFilePath, meshFileCache) { cachedMeshFile => cachedMeshFile.reader.int64().getAttr("/", "version") }.toOption.getOrElse(0) @@ -175,7 +176,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") - safeExecute(meshFilePath) { cachedMeshFile => + safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => val chunkPositionLiterals = cachedMeshFile.reader .`object`() .getAllGroupMembers(s"/${listMeshChunksRequest.segmentId}/$defaultLevelOfDetail") @@ -185,7 +186,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC }.flatten ?~> "mesh.file.open.failed" } - def listMeshChunksForSegmentV1(organizationName: String, + def listMeshChunksForSegmentV3(organizationName: String, dataSetName: String, dataLayerName: String, listMeshChunksRequest: ListMeshChunksRequest): Fox[WebknossosSegmentInfo] = { @@ -197,7 +198,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") - safeExecute(meshFilePath) { cachedMeshFile => + safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => val segmentId = listMeshChunksRequest.segmentId val encoding = cachedMeshFile.reader.string().getAttr("/", "mesh_format") val lodScaleMultiplier = cachedMeshFile.reader.float64().getAttr("/", "lod_scale_multiplier") @@ -210,15 +211,13 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .readArrayBlockWithOffset("/neuroglancer", (neuroglancerEnd - neuroglancerStart).toInt, neuroglancerStart) val segmentInfo = parseNeuroglancerManifest(manifest) val meshfile = getFragmentsFromSegmentInfo(segmentInfo, lodScaleMultiplier, neuroglancerStart) - meshfile - .map(meshInfo => WebknossosSegmentInfo(transform = transform, meshFormat = encoding, chunks = meshInfo)) - .toFox - }.flatten + WebknossosSegmentInfo(transform = transform, meshFormat = encoding, chunks = meshfile) + } } private def getFragmentsFromSegmentInfo(segmentInfo: NeuroglancerSegmentInfo, lodScaleMultiplier: Double, - neuroglancerOffsetStart: Long): Option[MeshSegmentInfo] = { + neuroglancerOffsetStart: Long): MeshSegmentInfo = { val totalMeshSize = segmentInfo.fragmentOffsets.map(_.sum).sum val meshByteStartOffset = neuroglancerOffsetStart - totalMeshSize val fragmentByteOffsets = segmentInfo.fragmentOffsets.map(_.scanLeft(0)(_ + _)) // This builds a cumulative sum @@ -250,13 +249,12 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC chunkShape = segmentInfo.chunkShape, fragments = fragments(lod))) .toList - Some(MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods)) + MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods) } private def parseNeuroglancerManifest(manifest: Array[Byte]): NeuroglancerSegmentInfo = { - // All Ints here should be UInt32 per spec. + // All Ints here should be UInt32 per spec. We assume that the sign bit is not necessary (the encoded values are at most 2^31). // But they all are used to index into Arrays and JVM doesn't allow for Long Array Indexes, // we can't convert them. - // TODO Check whether limit exceeded for the Ints. val byteInput = new ByteArrayInputStream(manifest) val dis = new LittleEndianDataInputStream(byteInput) @@ -280,7 +278,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC numFragmentsPerLod(lod) = dis.readInt() } - // TODO what if there are no fragments? val fragmentPositionsList = new ListBuffer[List[Vec3Int]] val fragmentSizes = new ListBuffer[List[Int]] for (lod <- 0 until numLods) { @@ -325,7 +322,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .uint64() .readMatrixBlockWithOffset("buckets", (bucketEnd - bucketStart + 1).toInt, 3, bucketStart, 0) - // TODO what if you don't find segment val bucketLocalOffset = buckets.map(_(0)).indexOf(segmentId) val neuroglancerStart = buckets(bucketLocalOffset)(1) val neuroglancerEnd = buckets(bucketLocalOffset)(2) @@ -344,7 +340,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${meshChunkDataRequest.meshFile}.$meshFileExtension") - safeExecute(meshFilePath) { cachedMeshFile => + safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => val encoding = cachedMeshFile.reader.string().getAttr("/", "metadata/encoding") val key = s"/${meshChunkDataRequest.segmentId}/$defaultLevelOfDetail/${positionLiteral(meshChunkDataRequest.position)}" @@ -353,10 +349,10 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC } ?~> "mesh.file.readData.failed" } - def readMeshChunkV1(organizationName: String, + def readMeshChunkV3(organizationName: String, dataSetName: String, dataLayerName: String, - meshChunkDataRequest: MeshChunkDataRequestV1, + meshChunkDataRequest: MeshChunkDataRequestV3, ): Fox[(Array[Byte], String)] = { val meshFilePath = dataBaseDir .resolve(organizationName) @@ -365,7 +361,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${meshChunkDataRequest.meshFile}.$meshFileExtension") - safeExecute(meshFilePath) { cachedMeshFile => + safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => val meshFormat = cachedMeshFile.reader.string().getAttr("/", "mesh_format") val data = cachedMeshFile.reader @@ -377,26 +373,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC } ?~> "mesh.file.readData.failed" } - private def safeExecute[T](filePath: Path)(block: CachedHdf5File => T): Fox[T] = - for { - _ <- bool2Fox(filePath.toFile.exists()) ?~> "mesh.file.open.failed" - result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { - block - }.toFox - } yield result - - private def safeExecuteBox[T](filePath: Path)(block: CachedHdf5File => T): Box[T] = - for { - _ <- if (filePath.toFile.exists()) { - Full(true) - } else { - Empty ~> "mesh.file.open.failed" - } - result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { - block - }.toOption - } yield result - private def positionLiteral(position: Vec3Int) = s"${position.x}_${position.y}_${position.z}" diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala index 7a76d160306..00569aa1c35 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala @@ -2,9 +2,14 @@ package com.scalableminds.webknossos.datastore.storage import ch.systemsx.cisd.hdf5.{HDF5FactoryProvider, IHDF5Reader} import com.scalableminds.util.cache.LRUConcurrentCache +import com.scalableminds.util.tools.Fox +import com.scalableminds.util.tools.Fox.{bool2Fox, try2Fox} import com.scalableminds.webknossos.datastore.dataformats.SafeCachable +import net.liftweb.common.{Box, Full, Empty} import java.nio.file.Path +import scala.concurrent.ExecutionContext +import scala.util.Using case class CachedHdf5File(reader: IHDF5Reader) extends SafeCachable with AutoCloseable { override protected def onFinalize(): Unit = reader.close() @@ -43,3 +48,26 @@ class Hdf5FileCache(val maxEntries: Int) extends LRUConcurrentCache[String, Cach } } } + +object CachedHdf5Utils { + def safeExecute[T](filePath: Path, meshFileCache: Hdf5FileCache)(block: CachedHdf5File => T)( + implicit ec: ExecutionContext): Fox[T] = + for { + _ <- bool2Fox(filePath.toFile.exists()) ?~> "mesh.file.open.failed" + result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { + block + }.toFox + } yield result + + def safeExecuteBox[T](filePath: Path, meshFileCache: Hdf5FileCache)(block: CachedHdf5File => T): Box[T] = + for { + _ <- if (filePath.toFile.exists()) { + Full(true) + } else { + Empty ~> "mesh.file.open.failed" + } + result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { + block + }.toOption + } yield result +} diff --git a/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes b/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes index b92f4d95825..c647910ae95 100644 --- a/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes +++ b/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes @@ -2,92 +2,92 @@ # Health endpoint -GET /health @com.scalableminds.webknossos.datastore.controllers.Application.health +GET /health @com.scalableminds.webknossos.datastore.controllers.Application.health # Read image data -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/data @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.requestViaWebKnossos(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/data @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.requestRawCuboid(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, x: Int, y: Int, z: Int, width: Int, height: Int, depth: Int, mag: String, halfByte: Boolean ?= false, mappingName: Option[String]) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/thumbnail.jpg @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.thumbnailJpeg(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, width: Int, height: Int, centerX: Option[Int], centerY: Option[Int], centerZ: Option[Int], zoom: Option[Double]) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/findData @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.findData(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/colorStatistics @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.colorStatistics(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/histogram @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.histogram(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/data @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.requestViaWebKnossos(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/data @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.requestRawCuboid(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, x: Int, y: Int, z: Int, width: Int, height: Int, depth: Int, mag: String, halfByte: Boolean ?= false, mappingName: Option[String]) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/thumbnail.jpg @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.thumbnailJpeg(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, width: Int, height: Int, centerX: Option[Int], centerY: Option[Int], centerZ: Option[Int], zoom: Option[Double]) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/findData @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.findData(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/colorStatistics @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.colorStatistics(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/histogram @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.histogram(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) # Knossos compatible routes -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/mag:resolution/x:x/y:y/z:z/bucket.raw @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.requestViaKnossos(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, resolution: Int, x: Int, y: Int, z: Int, cubeSize: Int) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/mag:resolution/x:x/y:y/z:z/bucket.raw @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.requestViaKnossos(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, resolution: Int, x: Int, y: Int, z: Int, cubeSize: Int) # Zarr compatible routes -GET /zarr/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataSourceFolderContents(token: Option[String], organizationName: String, dataSetName: String) -GET /zarr/:organizationName/:dataSetName/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataSourceFolderContents(token: Option[String], organizationName: String, dataSetName: String) -GET /zarr/:organizationName/:dataSetName/.zgroup @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestZGroup(token: Option[String], organizationName: String, dataSetName: String, dataLayerName="") -GET /zarr/:organizationName/:dataSetName/datasource-properties.json @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataSource(token: Option[String], organizationName: String, dataSetName: String) -GET /zarr/:organizationName/:dataSetName/:dataLayerName @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataLayerFolderContents(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /zarr/:organizationName/:dataSetName/:dataLayerName/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataLayerFolderContents(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /zarr/:organizationName/:dataSetName/:dataLayerName/.zattrs @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestZAttrs(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /zarr/:organizationName/:dataSetName/:dataLayerName/.zgroup @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestZGroup(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /zarr/:organizationName/:dataSetName/:dataLayerName/:mag @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataLayerMagFolderContents(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mag: String) -GET /zarr/:organizationName/:dataSetName/:dataLayerName/:mag/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataLayerMagFolderContents(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mag: String) -GET /zarr/:organizationName/:dataSetName/:dataLayerName/:mag/.zarray @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestZArray(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mag: String) -GET /zarr/:organizationName/:dataSetName/:dataLayerName/:mag/:cxyz @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestRawZarrCube(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mag: String, cxyz: String) +GET /zarr/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataSourceFolderContents(token: Option[String], organizationName: String, dataSetName: String) +GET /zarr/:organizationName/:dataSetName/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataSourceFolderContents(token: Option[String], organizationName: String, dataSetName: String) +GET /zarr/:organizationName/:dataSetName/.zgroup @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestZGroup(token: Option[String], organizationName: String, dataSetName: String, dataLayerName="") +GET /zarr/:organizationName/:dataSetName/datasource-properties.json @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataSource(token: Option[String], organizationName: String, dataSetName: String) +GET /zarr/:organizationName/:dataSetName/:dataLayerName @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataLayerFolderContents(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /zarr/:organizationName/:dataSetName/:dataLayerName/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataLayerFolderContents(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /zarr/:organizationName/:dataSetName/:dataLayerName/.zattrs @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestZAttrs(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /zarr/:organizationName/:dataSetName/:dataLayerName/.zgroup @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestZGroup(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /zarr/:organizationName/:dataSetName/:dataLayerName/:mag @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataLayerMagFolderContents(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mag: String) +GET /zarr/:organizationName/:dataSetName/:dataLayerName/:mag/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestDataLayerMagFolderContents(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mag: String) +GET /zarr/:organizationName/:dataSetName/:dataLayerName/:mag/.zarray @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestZArray(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mag: String) +GET /zarr/:organizationName/:dataSetName/:dataLayerName/:mag/:cxyz @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.requestRawZarrCube(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mag: String, cxyz: String) -GET /annotations/zarr/:accessTokenOrId @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataSourceFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String) -GET /annotations/zarr/:accessTokenOrId/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataSourceFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String) -GET /annotations/zarr/:accessTokenOrId/.zgroup @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.zGroupPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName="") -GET /annotations/zarr/:accessTokenOrId/datasource-properties.json @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataSourceWithAnnotationPrivateLink(token: Option[String], accessTokenOrId: String) -GET /annotations/zarr/:accessTokenOrId/:dataLayerName @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataLayerFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String) -GET /annotations/zarr/:accessTokenOrId/:dataLayerName/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataLayerFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String) -GET /annotations/zarr/:accessTokenOrId/:dataLayerName/.zattrs @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.zAttrsWithAnnotationPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String) -GET /annotations/zarr/:accessTokenOrId/:dataLayerName/.zgroup @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.zGroupPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String) -GET /annotations/zarr/:accessTokenOrId/:dataLayerName/:mag @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataLayerMagFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String, mag: String) -GET /annotations/zarr/:accessTokenOrId/:dataLayerName/:mag/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataLayerMagFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String, mag: String) -GET /annotations/zarr/:accessTokenOrId/:dataLayerName/:mag/.zarray @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.zArrayPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String, mag: String) -GET /annotations/zarr/:accessTokenOrId/:dataLayerName/:mag/:cxyz @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.rawZarrCubePrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String, mag: String, cxyz: String) +GET /annotations/zarr/:accessTokenOrId @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataSourceFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String) +GET /annotations/zarr/:accessTokenOrId/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataSourceFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String) +GET /annotations/zarr/:accessTokenOrId/.zgroup @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.zGroupPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName="") +GET /annotations/zarr/:accessTokenOrId/datasource-properties.json @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataSourceWithAnnotationPrivateLink(token: Option[String], accessTokenOrId: String) +GET /annotations/zarr/:accessTokenOrId/:dataLayerName @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataLayerFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String) +GET /annotations/zarr/:accessTokenOrId/:dataLayerName/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataLayerFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String) +GET /annotations/zarr/:accessTokenOrId/:dataLayerName/.zattrs @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.zAttrsWithAnnotationPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String) +GET /annotations/zarr/:accessTokenOrId/:dataLayerName/.zgroup @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.zGroupPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String) +GET /annotations/zarr/:accessTokenOrId/:dataLayerName/:mag @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataLayerMagFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String, mag: String) +GET /annotations/zarr/:accessTokenOrId/:dataLayerName/:mag/ @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.dataLayerMagFolderContentsPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String, mag: String) +GET /annotations/zarr/:accessTokenOrId/:dataLayerName/:mag/.zarray @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.zArrayPrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String, mag: String) +GET /annotations/zarr/:accessTokenOrId/:dataLayerName/:mag/:cxyz @com.scalableminds.webknossos.datastore.controllers.ZarrStreamingController.rawZarrCubePrivateLink(token: Option[String], accessTokenOrId: String, dataLayerName: String, mag: String, cxyz: String) # Segmentation mappings -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/mappings/:mappingName @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.mappingJson(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/mappings @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMappings(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/mappings/:mappingName @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.mappingJson(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/mappings @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMappings(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) # Agglomerate files -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listAgglomerates(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates/:mappingName/skeleton/:agglomerateId @com.scalableminds.webknossos.datastore.controllers.DataSourceController.generateAgglomerateSkeleton(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String, agglomerateId: Long) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates/:mappingName/agglomerateGraph/:agglomerateId @com.scalableminds.webknossos.datastore.controllers.DataSourceController.agglomerateGraph(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String, agglomerateId: Long) -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates/:mappingName/largestAgglomerateId @com.scalableminds.webknossos.datastore.controllers.DataSourceController.largestAgglomerateId(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates/:mappingName/agglomeratesForSegments @com.scalableminds.webknossos.datastore.controllers.DataSourceController.agglomerateIdsForSegmentIds(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listAgglomerates(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates/:mappingName/skeleton/:agglomerateId @com.scalableminds.webknossos.datastore.controllers.DataSourceController.generateAgglomerateSkeleton(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String, agglomerateId: Long) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates/:mappingName/agglomerateGraph/:agglomerateId @com.scalableminds.webknossos.datastore.controllers.DataSourceController.agglomerateGraph(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String, agglomerateId: Long) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates/:mappingName/largestAgglomerateId @com.scalableminds.webknossos.datastore.controllers.DataSourceController.largestAgglomerateId(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agglomerates/:mappingName/agglomeratesForSegments @com.scalableminds.webknossos.datastore.controllers.DataSourceController.agglomerateIdsForSegmentIds(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, mappingName: String) # Mesh files -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/v1/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentV1(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/v1/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkV1(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -GET /datasets/dummyDraco @com.scalableminds.webknossos.datastore.controllers.DataSourceController.dummyDracoFile() +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/formatVersion/:formatVersion/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentForVersion(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, formatVersion: Int) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/formatVersion/:formatVersion/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkForVersion(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, formatVersion: Int) +GET /datasets/dummyDraco @com.scalableminds.webknossos.datastore.controllers.DataSourceController.dummyDracoFile() # Connectome files -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listConnectomeFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes/synapses/positions @com.scalableminds.webknossos.datastore.controllers.DataSourceController.getSynapsePositions(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes/synapses/types @com.scalableminds.webknossos.datastore.controllers.DataSourceController.getSynapseTypes(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes/synapses/:direction @com.scalableminds.webknossos.datastore.controllers.DataSourceController.getSynapticPartnerForSynapses(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, direction: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes/synapses @com.scalableminds.webknossos.datastore.controllers.DataSourceController.getSynapsesForAgglomerates(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listConnectomeFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes/synapses/positions @com.scalableminds.webknossos.datastore.controllers.DataSourceController.getSynapsePositions(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes/synapses/types @com.scalableminds.webknossos.datastore.controllers.DataSourceController.getSynapseTypes(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes/synapses/:direction @com.scalableminds.webknossos.datastore.controllers.DataSourceController.getSynapticPartnerForSynapses(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, direction: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes/synapses @com.scalableminds.webknossos.datastore.controllers.DataSourceController.getSynapsesForAgglomerates(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) # Isosurfaces -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/isosurface @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.requestIsosurface(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/isosurface @com.scalableminds.webknossos.datastore.controllers.BinaryDataController.requestIsosurface(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) # DataSource management -POST /datasets @com.scalableminds.webknossos.datastore.controllers.DataSourceController.uploadChunk(token: Option[String]) -POST /datasets/reserveUpload @com.scalableminds.webknossos.datastore.controllers.DataSourceController.reserveUpload(token: Option[String]) -POST /datasets/finishUpload @com.scalableminds.webknossos.datastore.controllers.DataSourceController.finishUpload(token: Option[String]) -POST /datasets/cancelUpload @com.scalableminds.webknossos.datastore.controllers.DataSourceController.cancelUpload(token: Option[String]) -GET /datasets/:organizationName/:dataSetName/readInboxDataSourceLike @com.scalableminds.webknossos.datastore.controllers.DataSourceController.read(token: Option[String], organizationName: String, dataSetName: String, returnFormatLike: Boolean ?= true) -GET /datasets/:organizationName/:dataSetName/readInboxDataSource @com.scalableminds.webknossos.datastore.controllers.DataSourceController.read(token: Option[String], organizationName: String, dataSetName: String, returnFormatLike: Boolean ?= false) -POST /datasets/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.DataSourceController.update(token: Option[String], organizationName: String, dataSetName: String) -PUT /datasets/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.DataSourceController.add(token: Option[String], organizationName: String, dataSetName: String) -GET /datasets/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.DataSourceController.explore(token: Option[String], organizationName: String, dataSetName: String) -DELETE /datasets/:organizationName/:dataSetName/deleteOnDisk @com.scalableminds.webknossos.datastore.controllers.DataSourceController.deleteOnDisk(token: Option[String], organizationName: String, dataSetName: String) +POST /datasets @com.scalableminds.webknossos.datastore.controllers.DataSourceController.uploadChunk(token: Option[String]) +POST /datasets/reserveUpload @com.scalableminds.webknossos.datastore.controllers.DataSourceController.reserveUpload(token: Option[String]) +POST /datasets/finishUpload @com.scalableminds.webknossos.datastore.controllers.DataSourceController.finishUpload(token: Option[String]) +POST /datasets/cancelUpload @com.scalableminds.webknossos.datastore.controllers.DataSourceController.cancelUpload(token: Option[String]) +GET /datasets/:organizationName/:dataSetName/readInboxDataSourceLike @com.scalableminds.webknossos.datastore.controllers.DataSourceController.read(token: Option[String], organizationName: String, dataSetName: String, returnFormatLike: Boolean ?= true) +GET /datasets/:organizationName/:dataSetName/readInboxDataSource @com.scalableminds.webknossos.datastore.controllers.DataSourceController.read(token: Option[String], organizationName: String, dataSetName: String, returnFormatLike: Boolean ?= false) +POST /datasets/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.DataSourceController.update(token: Option[String], organizationName: String, dataSetName: String) +PUT /datasets/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.DataSourceController.add(token: Option[String], organizationName: String, dataSetName: String) +GET /datasets/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.DataSourceController.explore(token: Option[String], organizationName: String, dataSetName: String) +DELETE /datasets/:organizationName/:dataSetName/deleteOnDisk @com.scalableminds.webknossos.datastore.controllers.DataSourceController.deleteOnDisk(token: Option[String], organizationName: String, dataSetName: String) # Actions -POST /triggers/checkInboxBlocking @com.scalableminds.webknossos.datastore.controllers.DataSourceController.triggerInboxCheckBlocking(token: Option[String]) -POST /triggers/newOrganizationFolder @com.scalableminds.webknossos.datastore.controllers.DataSourceController.createOrganizationDirectory(token: Option[String], organizationName: String) -POST /triggers/reload/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.DataSourceController.reload(token: Option[String], organizationName: String, dataSetName: String, layerName: Option[String]) +POST /triggers/checkInboxBlocking @com.scalableminds.webknossos.datastore.controllers.DataSourceController.triggerInboxCheckBlocking(token: Option[String]) +POST /triggers/newOrganizationFolder @com.scalableminds.webknossos.datastore.controllers.DataSourceController.createOrganizationDirectory(token: Option[String], organizationName: String) +POST /triggers/reload/:organizationName/:dataSetName @com.scalableminds.webknossos.datastore.controllers.DataSourceController.reload(token: Option[String], organizationName: String, dataSetName: String, layerName: Option[String]) # Exports -GET /exports/:jobId/download @com.scalableminds.webknossos.datastore.controllers.ExportsController.download(token: Option[String], jobId: String) +GET /exports/:jobId/download @com.scalableminds.webknossos.datastore.controllers.ExportsController.download(token: Option[String], jobId: String) From 8e60a9d2e71b096882c43faa6943d3348c6e4153 Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 29 Sep 2022 14:31:50 +0200 Subject: [PATCH 16/63] rename chunkdatarequest and add custom writes to vec3float --- .../util/geometry/Vec3Float.scala | 23 +++++++++++++++++-- .../datastore/services/MeshFileService.scala | 8 +++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala index 40de93c024f..170a6afdc33 100644 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala @@ -1,6 +1,7 @@ package com.scalableminds.util.geometry -import play.api.libs.json.{Json, OFormat}; +import play.api.libs.json.Json.{fromJson, toJson} +import play.api.libs.json.{JsArray, JsError, JsPath, JsResult, JsSuccess, JsValue, JsonValidationError, Reads, Writes}; case class Vec3Float(x: Float, y: Float, z: Float) { def scale(s: Float): Vec3Float = Vec3Float(x * s, y * s, z * s) @@ -14,5 +15,23 @@ case class Vec3Float(x: Float, y: Float, z: Float) { } object Vec3Float { - implicit val jsonFormat: OFormat[Vec3Float] = Json.format[Vec3Float] + implicit object Vec3FloatReads extends Reads[Vec3Float] { + def reads(json: JsValue): JsResult[Vec3Float] = json match { + case JsArray(ts) if ts.size == 3 => + val c = ts.map(fromJson[Float](_)).flatMap(_.asOpt) + if (c.size != 3) + JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.array.invalidContent")))) + else + JsSuccess(Vec3Float(c(0), c(1), c(2))) + case _ => + JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.expected.vec3FloatArray")))) + } + } + + implicit object Vec3FloatWrites extends Writes[Vec3Float] { + def writes(v: Vec3Float): JsArray = { + val l = List(v.x, v.y, v.z) + JsArray(l.map(toJson(_))) + } + } } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 5ca3eaf546a..bc87fcfce21 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -41,8 +41,8 @@ case class MeshChunkDataRequestV0( case class MeshChunkDataRequestV3( meshFile: String, - fragmentStartOffset: Long, - fragmentSize: Int + byteOffset: Long, + byteSize: Int ) object MeshChunkDataRequestV0 { @@ -367,8 +367,8 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC cachedMeshFile.reader .uint8() .readArrayBlockWithOffset("neuroglancer", - meshChunkDataRequest.fragmentSize, - meshChunkDataRequest.fragmentStartOffset) + meshChunkDataRequest.byteSize, + meshChunkDataRequest.byteOffset) (data, meshFormat) } ?~> "mesh.file.readData.failed" } From 72e325d6fe3ba252b270fc0d6e9ed4477473867d Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 16:32:13 +0200 Subject: [PATCH 17/63] use new mesh v3 api --- frontend/javascripts/admin/admin_rest_api.ts | 102 +-------------- frontend/javascripts/libs/DRACOLoader.js | 6 +- .../oxalis/controller/scene_controller.ts | 10 ++ .../oxalis/model/sagas/isosurface_saga.ts | 123 ++++++++++++------ .../segments_tab/segments_view_helper.tsx | 8 +- 5 files changed, 111 insertions(+), 138 deletions(-) diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts index 1e29007d453..41e8804146d 100644 --- a/frontend/javascripts/admin/admin_rest_api.ts +++ b/frontend/javascripts/admin/admin_rest_api.ts @@ -91,9 +91,14 @@ import messages from "messages"; import window, { location } from "libs/window"; import { SaveQueueType } from "oxalis/model/actions/save_actions"; import { DatasourceConfiguration } from "types/schemas/datasource.types"; +import { doWithToken } from "./api/token"; const MAX_SERVER_ITEMS_PER_RESPONSE = 1000; +export * from "./api/token"; +export * as meshV3 from "./api/mesh_v3"; +export * as meshV0 from "./api/mesh_v0"; + type NewTeam = { readonly name: string; }; @@ -107,58 +112,6 @@ function assertResponseLimit(collection: unknown[]) { } // ### Do with userToken -let tokenRequestPromise: Promise | null; - -function requestUserToken(): Promise { - if (tokenRequestPromise) { - return tokenRequestPromise; - } - - tokenRequestPromise = Request.receiveJSON("/api/userToken/generate", { - method: "POST", - }).then((tokenObj) => { - tokenRequestPromise = null; - return tokenObj.token as string; - }); - - return tokenRequestPromise; -} - -export function getSharingTokenFromUrlParameters(): string | null | undefined { - if (location != null) { - const params = Utils.getUrlParamsObject(); - - if (params != null && params.token != null) { - return params.token; - } - } - - return null; -} - -let tokenPromise: Promise; -export function doWithToken(fn: (token: string) => Promise, tries: number = 1): Promise { - const sharingToken = getSharingTokenFromUrlParameters(); - - if (sharingToken != null) { - return fn(sharingToken); - } - - if (!tokenPromise) tokenPromise = requestUserToken(); - return tokenPromise.then(fn).catch((error) => { - if (error.status === 403) { - console.warn("Token expired. Requesting new token..."); - tokenPromise = requestUserToken(); - - // If three new tokens did not fix the 403, abort, otherwise we'll get into an endless loop here - if (tries < 3) { - return doWithToken(fn, tries + 1); - } - } - - throw error; - }); -} export function sendAnalyticsEvent(eventType: string, eventProperties: {} = {}): void { // Note that the Promise from sendJSONReceiveJSON is not awaited or returned here, @@ -2130,51 +2083,6 @@ export function getMeshfilesForDatasetLayer( ); } -export function getMeshfileChunksForSegment( - dataStoreUrl: string, - datasetId: APIDatasetId, - layerName: string, - meshFile: string, - segmentId: number, -): Promise> { - return doWithToken((token) => - Request.sendJSONReceiveJSON( - `${dataStoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${layerName}/meshes/chunks?token=${token}`, - { - data: { - meshFile, - segmentId, - }, - showErrorToast: false, - }, - ), - ); -} - -export function getMeshfileChunkData( - dataStoreUrl: string, - datasetId: APIDatasetId, - layerName: string, - meshFile: string, - segmentId: number, - position: Vector3, -): Promise { - return doWithToken(async (token) => { - const data = await Request.sendJSONReceiveArraybufferWithHeaders( - `${dataStoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${layerName}/meshes/chunks/data?token=${token}`, - { - data: { - meshFile, - segmentId, - position, - }, - useWebworkerForArrayBuffer: false, - }, - ); - return data; - }); -} - export function getDummyDraco(): Promise { return doWithToken(async (token) => { const data = await Request.receiveArraybuffer(`/assets/draco_file.bin`, { diff --git a/frontend/javascripts/libs/DRACOLoader.js b/frontend/javascripts/libs/DRACOLoader.js index c04219bf122..d39552db1cf 100644 --- a/frontend/javascripts/libs/DRACOLoader.js +++ b/frontend/javascripts/libs/DRACOLoader.js @@ -65,7 +65,11 @@ import * as THREE from "three"; attributeTypes: attributeTypes || this.defaultAttributeTypes, useUniqueIDs: !!attributeIDs, }; - return this.decodeGeometry(buffer, taskConfig).then(callback); + const promise = this.decodeGeometry(buffer, taskConfig); + if (callback == null) { + return promise; + } + return promise.then(callback); } decodeGeometry(buffer, taskConfig) { diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 0c34490a033..904f7b095d2 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -272,6 +272,8 @@ class SceneController { geometry: THREE.BufferGeometry, segmentationId: number, passive: boolean = false, + offset: Vector3 | null = null, + useDatasetScale: boolean = false, ): void { if (this.isosurfacesGroupsPerSegmentationId[segmentationId] == null) { const newGroup = new THREE.Group(); @@ -281,8 +283,16 @@ class SceneController { newGroup.cellId = segmentationId; // @ts-ignore newGroup.passive = passive; + if (useDatasetScale) { + newGroup.scale.copy(new THREE.Vector3(...Store.getState().dataset.dataSource.scale)); + } } const mesh = this.constructIsosurfaceMesh(segmentationId, geometry, passive); + if (offset) { + mesh.translateX(offset[0]); + mesh.translateY(offset[1]); + mesh.translateZ(offset[2]); + } this.isosurfacesGroupsPerSegmentationId[segmentationId].add(mesh); } diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index f000cf2081d..e2387bb8fd8 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -1,7 +1,7 @@ import { saveAs } from "file-saver"; import _ from "lodash"; import { V3 } from "libs/mjs"; -import { sleep } from "libs/utils"; +import { point3ToVector3, sleep } from "libs/utils"; import ErrorHandling from "libs/error_handling"; import type { APIDataLayer } from "types/api_flow_types"; import "libs/DRACOLoader.js"; @@ -42,9 +42,9 @@ import { stlIsosurfaceConstants } from "oxalis/view/right-border-tabs/segments_t import { computeIsosurface, sendAnalyticsEvent, - getMeshfileChunksForSegment, - getMeshfileChunkData, getDummyDraco, + meshV0, + meshV3, } from "admin/admin_rest_api"; import { getFlooredPosition } from "oxalis/model/accessors/flycam_accessor"; import { setImportingMeshStateAction } from "oxalis/model/actions/ui_actions"; @@ -551,17 +551,30 @@ function* loadPrecomputedMeshForSegmentId( yield* put(addPrecomputedIsosurfaceAction(layerName, id, seedPosition, meshFileName)); yield* put(startedLoadingIsosurfaceAction(layerName, id)); const dataset = yield* select((state) => state.dataset); - let availableChunks = null; + let availableChunks = null; + const version = 3; try { - availableChunks = yield* call( - getMeshfileChunksForSegment, - dataset.dataStore.url, - dataset, - getBaseSegmentationName(segmentationLayer), - meshFileName, - id, - ); + if (version === 3) { + const segmentInfo = yield* call( + meshV3.getMeshfileChunksForSegment, + dataset.dataStore.url, + dataset, + getBaseSegmentationName(segmentationLayer), + meshFileName, + id, + ); + availableChunks = _.first(segmentInfo.chunks.lods)?.fragments || []; + } else { + availableChunks = yield* call( + meshV0.getMeshfileChunksForSegment, + dataset.dataStore.url, + dataset, + getBaseSegmentationName(segmentationLayer), + meshFileName, + id, + ); + } } catch (exception) { console.warn("Mesh chunk couldn't be loaded due to", exception); Toast.warning(messages["tracing.mesh_listing_failed"]); @@ -571,29 +584,54 @@ function* loadPrecomputedMeshForSegmentId( } // Sort the chunks by distance to the seedPosition, so that the mesh loads from the inside out - const sortedAvailableChunks = _.sortBy(availableChunks, (chunkPosition) => - V3.length(V3.sub(seedPosition, chunkPosition)), - ); + const sortedAvailableChunks = _.sortBy(availableChunks, (chunk: Vector3 | meshV3.MeshFragment) => + V3.length(V3.sub(seedPosition, "position" in chunk ? point3ToVector3(chunk.position) : chunk)), + ) as Array | Array; const tasks = sortedAvailableChunks.map( - (chunkPosition) => + (chunk) => function* loadChunk() { - const stlData = yield* call( - getMeshfileChunkData, - dataset.dataStore.url, - dataset, - getBaseSegmentationName(segmentationLayer), - meshFileName, - id, - chunkPosition, - ); - const geometry = yield* call(parseStlBuffer, stlData); const sceneController = yield* call(getSceneController); - yield* call( - { context: sceneController, fn: sceneController.addIsosurfaceFromGeometry }, - geometry, - id, - ); + + if ("position" in chunk) { + const dracoData = yield* call( + meshV3.getMeshfileChunkData, + dataset.dataStore.url, + dataset, + getBaseSegmentationName(segmentationLayer), + meshFileName, + chunk.byteOffset, + chunk.byteSize, + ); + const loader = getDracoLoader(); + + const geometry = yield* call(loader.decodeDracoFile.bind(loader), dracoData); + yield* call( + { context: sceneController, fn: sceneController.addIsosurfaceFromGeometry }, + geometry, + id, + false, + point3ToVector3(chunk.position), + true, + ); + } else { + const stlData = yield* call( + meshV0.getMeshfileChunkData, + dataset.dataStore.url, + dataset, + getBaseSegmentationName(segmentationLayer), + meshFileName, + id, + chunk, + ); + const geometry = yield* call(parseStlBuffer, stlData); + yield* call( + { context: sceneController, fn: sceneController.addIsosurfaceFromGeometry }, + geometry, + id, + false, + ); + } }, ); @@ -607,18 +645,25 @@ function* loadPrecomputedMeshForSegmentId( yield* put(finishedLoadingIsosurfaceAction(layerName, id)); } -function* addDummyDraco() { - const buffer = yield* call(getDummyDraco); - console.log("buffer", buffer); - - const loader = new THREE.DRACOLoader(); - console.log("THREE.DRACOLoader", loader); +let _dracoLoader; +function getDracoLoader() { + if (_dracoLoader) { + return _dracoLoader; + } + _dracoLoader = new THREE.DRACOLoader(); - loader.setDecoderPath( + _dracoLoader.setDecoderPath( "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/", ); - loader.setDecoderConfig({ type: "js" }); + _dracoLoader.setDecoderConfig({ type: "js" }); + return _dracoLoader; +} + +function* addDummyDraco() { + const buffer = yield* call(getDummyDraco); + console.log("buffer", buffer); + const loader = getDracoLoader(); const sceneController = yield* call(getSceneController); // const url = "/data/datasets/dummyDraco"; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx index 721a109ae4c..5512f158db6 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx @@ -84,7 +84,13 @@ export function withMappingActivationConfirmation> // If the mapping name is undefined, no mapping is specified. In that case never show the activation modal. // In contrast, if the mapping name is null, this indicates that all mappings should be specifically disabled. - if (mappingName === undefined || layerName == null || mappingName === enabledMappingName) { + // todo: remove last condition + if ( + mappingName === undefined || + layerName == null || + mappingName === enabledMappingName || + mappingName == "" + ) { // @ts-expect-error ts-migrate(2322) FIXME: Type 'Omit, ... Remove this comment to see the full error message return ; } From de2c466ea85e37ba75b57b3df85dad9d9b14d373 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 16:32:26 +0200 Subject: [PATCH 18/63] temporary workaround for mapping name with empty string --- frontend/javascripts/oxalis/api/api_latest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/api/api_latest.ts b/frontend/javascripts/oxalis/api/api_latest.ts index c2762f3df2c..3bafa75c9dc 100644 --- a/frontend/javascripts/oxalis/api/api_latest.ts +++ b/frontend/javascripts/oxalis/api/api_latest.ts @@ -1841,7 +1841,8 @@ class DataApi { const { mappingName, meshFileName } = currentMeshFile; - if (mappingName != null) { + // todo: remove second condition + if (mappingName != null && mappingName != "") { const activeMapping = this.getActiveMapping(effectiveLayerName); if (mappingName !== activeMapping) { From 8b55ab46cd059fe75ea318270ab9cbca8607632f Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 16:37:17 +0200 Subject: [PATCH 19/63] adapt frontend to backend changes --- frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index e2387bb8fd8..446177787de 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -1,7 +1,7 @@ import { saveAs } from "file-saver"; import _ from "lodash"; import { V3 } from "libs/mjs"; -import { point3ToVector3, sleep } from "libs/utils"; +import { sleep } from "libs/utils"; import ErrorHandling from "libs/error_handling"; import type { APIDataLayer } from "types/api_flow_types"; import "libs/DRACOLoader.js"; @@ -585,7 +585,7 @@ function* loadPrecomputedMeshForSegmentId( // Sort the chunks by distance to the seedPosition, so that the mesh loads from the inside out const sortedAvailableChunks = _.sortBy(availableChunks, (chunk: Vector3 | meshV3.MeshFragment) => - V3.length(V3.sub(seedPosition, "position" in chunk ? point3ToVector3(chunk.position) : chunk)), + V3.length(V3.sub(seedPosition, "position" in chunk ? chunk.position : chunk)), ) as Array | Array; const tasks = sortedAvailableChunks.map( @@ -611,7 +611,7 @@ function* loadPrecomputedMeshForSegmentId( geometry, id, false, - point3ToVector3(chunk.position), + chunk.position, true, ); } else { From a8bfbac20f55db9375d59cc4c882c55eb3e726bd Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 16:37:35 +0200 Subject: [PATCH 20/63] add new api modules --- frontend/javascripts/admin/api/mesh_v0.ts | 49 ++++++++++++++++ frontend/javascripts/admin/api/mesh_v3.ts | 70 +++++++++++++++++++++++ frontend/javascripts/admin/api/token.ts | 56 ++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 frontend/javascripts/admin/api/mesh_v0.ts create mode 100644 frontend/javascripts/admin/api/mesh_v3.ts create mode 100644 frontend/javascripts/admin/api/token.ts diff --git a/frontend/javascripts/admin/api/mesh_v0.ts b/frontend/javascripts/admin/api/mesh_v0.ts new file mode 100644 index 00000000000..933d2187d9f --- /dev/null +++ b/frontend/javascripts/admin/api/mesh_v0.ts @@ -0,0 +1,49 @@ +import Request from "libs/request"; +import { Vector3 } from "oxalis/constants"; +import { APIDatasetId } from "types/api_flow_types"; +import { doWithToken } from "./token"; + +export function getMeshfileChunksForSegment( + dataStoreUrl: string, + datasetId: APIDatasetId, + layerName: string, + meshFile: string, + segmentId: number, +): Promise> { + return doWithToken((token) => + Request.sendJSONReceiveJSON( + `${dataStoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${layerName}/meshes/chunks?token=${token}`, + { + data: { + meshFile, + segmentId, + }, + showErrorToast: false, + }, + ), + ); +} + +export function getMeshfileChunkData( + dataStoreUrl: string, + datasetId: APIDatasetId, + layerName: string, + meshFile: string, + segmentId: number, + position: Vector3, +): Promise { + return doWithToken(async (token) => { + const data = await Request.sendJSONReceiveArraybufferWithHeaders( + `${dataStoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${layerName}/meshes/chunks/data?token=${token}`, + { + data: { + meshFile, + segmentId, + position, + }, + useWebworkerForArrayBuffer: false, + }, + ); + return data; + }); +} diff --git a/frontend/javascripts/admin/api/mesh_v3.ts b/frontend/javascripts/admin/api/mesh_v3.ts new file mode 100644 index 00000000000..0cb80696b83 --- /dev/null +++ b/frontend/javascripts/admin/api/mesh_v3.ts @@ -0,0 +1,70 @@ +import Request from "libs/request"; +import { Vector3 } from "oxalis/constants"; +import { APIDatasetId } from "types/api_flow_types"; +import { doWithToken } from "./token"; + +export type MeshFragment = { position: Vector3; byteOffset: number; byteSize: number }; + +type MeshLodInfo = { + scale: number; + vertexOffset: Vector3; + chunkShape: Vector3; + fragments: Array; +}; + +type MeshSegmentInfo = { + chunkShape: Vector3; + gridOrigin: Vector3; + lods: Array; +}; + +type SegmentInfo = { + transform: number[][]; // 3x3 matrix + meshFormat: "draco"; + chunks: MeshSegmentInfo; +}; + +export function getMeshfileChunksForSegment( + dataStoreUrl: string, + datasetId: APIDatasetId, + layerName: string, + meshFile: string, + segmentId: number, +): Promise { + return doWithToken((token) => + Request.sendJSONReceiveJSON( + `${dataStoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${layerName}/meshes/formatVersion/3/chunks?token=${token}`, + { + data: { + meshFile, + segmentId, + }, + showErrorToast: false, + }, + ), + ); +} + +export function getMeshfileChunkData( + dataStoreUrl: string, + datasetId: APIDatasetId, + layerName: string, + meshFile: string, + byteOffset: number, + byteSize: number, +): Promise { + return doWithToken(async (token) => { + const data = await Request.sendJSONReceiveArraybufferWithHeaders( + `${dataStoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${layerName}/meshes/formatVersion/3/chunks/data?token=${token}`, + { + data: { + meshFile, + byteOffset, + byteSize, + }, + useWebworkerForArrayBuffer: false, + }, + ); + return data; + }); +} diff --git a/frontend/javascripts/admin/api/token.ts b/frontend/javascripts/admin/api/token.ts new file mode 100644 index 00000000000..4424ca662c6 --- /dev/null +++ b/frontend/javascripts/admin/api/token.ts @@ -0,0 +1,56 @@ +import Request from "libs/request"; +import * as Utils from "libs/utils"; + +let tokenPromise: Promise; + +let tokenRequestPromise: Promise | null; + +function requestUserToken(): Promise { + if (tokenRequestPromise) { + return tokenRequestPromise; + } + + tokenRequestPromise = Request.receiveJSON("/api/userToken/generate", { + method: "POST", + }).then((tokenObj) => { + tokenRequestPromise = null; + return tokenObj.token as string; + }); + + return tokenRequestPromise; +} + +export function getSharingTokenFromUrlParameters(): string | null | undefined { + if (location != null) { + const params = Utils.getUrlParamsObject(); + + if (params != null && params.token != null) { + return params.token; + } + } + + return null; +} + +export function doWithToken(fn: (token: string) => Promise, tries: number = 1): Promise { + const sharingToken = getSharingTokenFromUrlParameters(); + + if (sharingToken != null) { + return fn(sharingToken); + } + + if (!tokenPromise) tokenPromise = requestUserToken(); + return tokenPromise.then(fn).catch((error) => { + if (error.status === 403) { + console.warn("Token expired. Requesting new token..."); + tokenPromise = requestUserToken(); + + // If three new tokens did not fix the 403, abort, otherwise we'll get into an endless loop here + if (tries < 3) { + return doWithToken(fn, tries + 1); + } + } + + throw error; + }); +} From 65081e97714312e217ae4e631b85e88b0e8cf293 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 16:39:21 +0200 Subject: [PATCH 21/63] remove the dummy draco file code again --- frontend/javascripts/admin/admin_rest_api.ts | 17 --------- .../oxalis/model/sagas/isosurface_saga.ts | 38 ++----------------- .../oxalis/view/action_bar_view.tsx | 4 -- 3 files changed, 4 insertions(+), 55 deletions(-) diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts index 41e8804146d..6e560f3b50e 100644 --- a/frontend/javascripts/admin/admin_rest_api.ts +++ b/frontend/javascripts/admin/admin_rest_api.ts @@ -2083,23 +2083,6 @@ export function getMeshfilesForDatasetLayer( ); } -export function getDummyDraco(): Promise { - return doWithToken(async (token) => { - const data = await Request.receiveArraybuffer(`/assets/draco_file.bin`, { - // const data = await Request.receiveArraybuffer(`/data/datasets/dummyDraco?token=${token}`, { - useWebworkerForArrayBuffer: false, - }); - - console.log("works", data, new Uint8Array(data)); - - const data2 = await Request.receiveArraybuffer(`/data/datasets/dummyDraco?token=${token}`, { - useWebworkerForArrayBuffer: false, - }); - console.log("doesnt work", data2, new Uint8Array(data2)); - return data2; - }); -} - // ### Connectomes export function getConnectomeFilesForDatasetLayer( dataStoreUrl: string, diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 446177787de..9959fd6adac 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -39,13 +39,7 @@ import type { Saga } from "oxalis/model/sagas/effect-generators"; import { select } from "oxalis/model/sagas/effect-generators"; import { actionChannel, takeEvery, call, take, race, put } from "typed-redux-saga"; import { stlIsosurfaceConstants } from "oxalis/view/right-border-tabs/segments_tab/segments_view"; -import { - computeIsosurface, - sendAnalyticsEvent, - getDummyDraco, - meshV0, - meshV3, -} from "admin/admin_rest_api"; +import { computeIsosurface, sendAnalyticsEvent, meshV0, meshV3 } from "admin/admin_rest_api"; import { getFlooredPosition } from "oxalis/model/accessors/flycam_accessor"; import { setImportingMeshStateAction } from "oxalis/model/actions/ui_actions"; import { zoomedAddressToAnotherZoomStepWithInfo } from "oxalis/model/helpers/position_converter"; @@ -656,34 +650,11 @@ function getDracoLoader() { "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/", ); _dracoLoader.setDecoderConfig({ type: "js" }); - return _dracoLoader; -} - -function* addDummyDraco() { - const buffer = yield* call(getDummyDraco); - console.log("buffer", buffer); - - const loader = getDracoLoader(); - const sceneController = yield* call(getSceneController); - // const url = "/data/datasets/dummyDraco"; - const url = "/assets/draco_file.bin"; - // const url = "http://localhost:9000/assets/bunny.drc"; - // loader.load(url, (geometry) => { - loader.decodeDracoFile(buffer, (geometry) => { - // geometry.computeVertexNormals(); + // todo: never dispose? + // loader.dispose(); - // todo: use correct id - sceneController.addIsosurfaceFromGeometry(geometry, 2347819234); - - // Release decoder resources. - loader.dispose(); - }); - - // const decoderModule = yield* call(() => draco3d.createDecoderModule({})); - // let decoder = new decoderModule.Decoder(); - // console.log("decoder"); - // const geometry = yield* call(parseStlBuffer, stlData); + return _dracoLoader; } /* @@ -773,5 +744,4 @@ export default function* isosurfaceSaga(): Saga { yield* takeEvery("UPDATE_ISOSURFACE_VISIBILITY", handleIsosurfaceVisibilityChange); yield* takeEvery(["START_EDITING", "COPY_SEGMENTATION_LAYER"], markEditedCellAsDirty); yield* takeEvery(["UPDATE_SEGMENT"], handleIsosurfaceColorChange); - yield* takeEvery(["ADD_DUMMY_DRACO"], addDummyDraco); } diff --git a/frontend/javascripts/oxalis/view/action_bar_view.tsx b/frontend/javascripts/oxalis/view/action_bar_view.tsx index bdcc58c0de4..11a6ec0519e 100644 --- a/frontend/javascripts/oxalis/view/action_bar_view.tsx +++ b/frontend/javascripts/oxalis/view/action_bar_view.tsx @@ -160,10 +160,6 @@ class ActionBarView extends React.PureComponent { {!isReadOnly && constants.MODES_PLANE.indexOf(viewMode) > -1 ? : null} {isArbitrarySupported && !is2d ? : null} {isViewMode ? this.renderStartTracingButton() : null} - - Store.dispatch({ type: "ADD_DUMMY_DRACO" })}> - ADD_DUMMY_DRACO - Date: Thu, 29 Sep 2022 16:45:21 +0200 Subject: [PATCH 22/63] fix addition --- .../main/scala/com/scalableminds/util/geometry/Vec3Float.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala index 170a6afdc33..7b2fe6ef557 100644 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Float.scala @@ -9,7 +9,7 @@ case class Vec3Float(x: Float, y: Float, z: Float) { def *(s: Float): Vec3Float = scale(s) def *(s: Double): Vec3Float = scale(s.toFloat) def *(that: Vec3Float): Vec3Float = Vec3Float(x * that.x, y * that.y, z * that.z) - def +(that: Vec3Float): Vec3Float = Vec3Float(x + that.x, y + that.y, z + that.y) + def +(that: Vec3Float): Vec3Float = Vec3Float(x + that.x, y + that.y, z + that.z) def toList: List[Float] = List(x, y, z) } From d269280a35e71327f84a58c15ec149e83e8854c3 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 16:58:06 +0200 Subject: [PATCH 23/63] use DRACOLoader from threejs package --- frontend/javascripts/libs/DRACOLoader.js | 471 ------------------ .../oxalis/model/sagas/isosurface_saga.ts | 18 +- public/draco_file.bin | Bin 251 -> 0 bytes 3 files changed, 15 insertions(+), 474 deletions(-) delete mode 100644 frontend/javascripts/libs/DRACOLoader.js delete mode 100644 public/draco_file.bin diff --git a/frontend/javascripts/libs/DRACOLoader.js b/frontend/javascripts/libs/DRACOLoader.js deleted file mode 100644 index d39552db1cf..00000000000 --- a/frontend/javascripts/libs/DRACOLoader.js +++ /dev/null @@ -1,471 +0,0 @@ -import * as THREE from "three"; - -(function () { - const _taskCache = new WeakMap(); - - class DRACOLoader extends THREE.Loader { - constructor(manager) { - super(manager); - this.decoderPath = ""; - this.decoderConfig = {}; - this.decoderBinary = null; - this.decoderPending = null; - this.workerLimit = 4; - this.workerPool = []; - this.workerNextTaskID = 1; - this.workerSourceURL = ""; - this.defaultAttributeIDs = { - position: "POSITION", - normal: "NORMAL", - color: "COLOR", - uv: "TEX_COORD", - }; - this.defaultAttributeTypes = { - position: "Float32Array", - normal: "Float32Array", - color: "Float32Array", - uv: "Float32Array", - }; - } - - setDecoderPath(path) { - this.decoderPath = path; - return this; - } - - setDecoderConfig(config) { - this.decoderConfig = config; - return this; - } - - setWorkerLimit(workerLimit) { - this.workerLimit = workerLimit; - return this; - } - - load(url, onLoad, onProgress, onError) { - const loader = new THREE.FileLoader(this.manager); - loader.setPath(this.path); - loader.setResponseType("arraybuffer"); - loader.setRequestHeader(this.requestHeader); - loader.setWithCredentials(this.withCredentials); - loader.load( - url, - (buffer) => { - this.decodeDracoFile(buffer, onLoad).catch(onError); - }, - onProgress, - onError, - ); - } - - decodeDracoFile(buffer, callback, attributeIDs, attributeTypes) { - const taskConfig = { - attributeIDs: attributeIDs || this.defaultAttributeIDs, - attributeTypes: attributeTypes || this.defaultAttributeTypes, - useUniqueIDs: !!attributeIDs, - }; - const promise = this.decodeGeometry(buffer, taskConfig); - if (callback == null) { - return promise; - } - return promise.then(callback); - } - - decodeGeometry(buffer, taskConfig) { - const taskKey = JSON.stringify(taskConfig); // Check for an existing task using this buffer. A transferred buffer cannot be transferred - // again from this thread. - - if (_taskCache.has(buffer)) { - const cachedTask = _taskCache.get(buffer); - - if (cachedTask.key === taskKey) { - return cachedTask.promise; - } else if (buffer.byteLength === 0) { - // Technically, it would be possible to wait for the previous task to complete, - // transfer the buffer back, and decode again with the second configuration. That - // is complex, and I don't know of any reason to decode a Draco buffer twice in - // different ways, so this is left unimplemented. - throw new Error( - "THREE.DRACOLoader: Unable to re-decode a buffer with different " + - "settings. Buffer has already been transferred.", - ); - } - } // - - let worker; - const taskID = this.workerNextTaskID++; - const taskCost = buffer.byteLength; // Obtain a worker and assign a task, and construct a geometry instance - // when the task completes. - - const geometryPending = this._getWorker(taskID, taskCost) - .then((_worker) => { - worker = _worker; - return new Promise((resolve, reject) => { - worker._callbacks[taskID] = { - resolve, - reject, - }; - worker.postMessage( - { - type: "decode", - id: taskID, - taskConfig, - buffer, - }, - [buffer], - ); // this.debug(); - }); - }) - .then((message) => this._createGeometry(message.geometry)); // Remove task from the task list. - // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416) - - geometryPending - .catch(() => true) - .then(() => { - if (worker && taskID) { - this._releaseTask(worker, taskID); // this.debug(); - } - }); // Cache the task result. - - _taskCache.set(buffer, { - key: taskKey, - promise: geometryPending, - }); - - return geometryPending; - } - - _createGeometry(geometryData) { - const geometry = new THREE.BufferGeometry(); - - if (geometryData.index) { - geometry.setIndex(new THREE.BufferAttribute(geometryData.index.array, 1)); - } - - for (let i = 0; i < geometryData.attributes.length; i++) { - const attribute = geometryData.attributes[i]; - const name = attribute.name; - const array = attribute.array; - const itemSize = attribute.itemSize; - geometry.setAttribute(name, new THREE.BufferAttribute(array, itemSize)); - } - - return geometry; - } - - _loadLibrary(url, responseType) { - const loader = new THREE.FileLoader(this.manager); - loader.setPath(this.decoderPath); - loader.setResponseType(responseType); - loader.setWithCredentials(this.withCredentials); - return new Promise((resolve, reject) => { - loader.load(url, resolve, undefined, reject); - }); - } - - preload() { - this._initDecoder(); - - return this; - } - - _initDecoder() { - if (this.decoderPending) return this.decoderPending; - const useJS = typeof WebAssembly !== "object" || this.decoderConfig.type === "js"; - const librariesPending = []; - - if (useJS) { - librariesPending.push(this._loadLibrary("draco_decoder.js", "text")); - } else { - librariesPending.push(this._loadLibrary("draco_wasm_wrapper.js", "text")); - librariesPending.push(this._loadLibrary("draco_decoder.wasm", "arraybuffer")); - } - - this.decoderPending = Promise.all(librariesPending).then((libraries) => { - const jsContent = libraries[0]; - - if (!useJS) { - this.decoderConfig.wasmBinary = libraries[1]; - } - - const fn = DRACOWorker.toString(); - const body = [ - "/* draco decoder */", - jsContent, - "", - "/* worker */", - fn.substring(fn.indexOf("{") + 1, fn.lastIndexOf("}")), - ].join("\n"); - this.workerSourceURL = URL.createObjectURL(new Blob([body])); - }); - return this.decoderPending; - } - - _getWorker(taskID, taskCost) { - return this._initDecoder().then(() => { - if (this.workerPool.length < this.workerLimit) { - const worker = new Worker(this.workerSourceURL); - worker._callbacks = {}; - worker._taskCosts = {}; - worker._taskLoad = 0; - worker.postMessage({ - type: "init", - decoderConfig: this.decoderConfig, - }); - - worker.onmessage = function (e) { - const message = e.data; - - switch (message.type) { - case "decode": - worker._callbacks[message.id].resolve(message); - - break; - - case "error": - worker._callbacks[message.id].reject(message); - - break; - - default: - console.error('THREE.DRACOLoader: Unexpected message, "' + message.type + '"'); - } - }; - - this.workerPool.push(worker); - } else { - this.workerPool.sort(function (a, b) { - return a._taskLoad > b._taskLoad ? -1 : 1; - }); - } - - const worker = this.workerPool[this.workerPool.length - 1]; - worker._taskCosts[taskID] = taskCost; - worker._taskLoad += taskCost; - return worker; - }); - } - - _releaseTask(worker, taskID) { - worker._taskLoad -= worker._taskCosts[taskID]; - delete worker._callbacks[taskID]; - delete worker._taskCosts[taskID]; - } - - debug() { - console.log( - "Task load: ", - this.workerPool.map((worker) => worker._taskLoad), - ); - } - - dispose() { - for (let i = 0; i < this.workerPool.length; ++i) { - this.workerPool[i].terminate(); - } - - this.workerPool.length = 0; - return this; - } - } - /* WEB WORKER */ - - function DRACOWorker() { - let decoderConfig; - let decoderPending; - - onmessage = function (e) { - const message = e.data; - - switch (message.type) { - case "init": - decoderConfig = message.decoderConfig; - decoderPending = new Promise(function ( - resolve, - /*, reject*/ - ) { - decoderConfig.onModuleLoaded = function (draco) { - // Module is Promise-like. Wrap before resolving to avoid loop. - resolve({ - draco: draco, - }); - }; - - DracoDecoderModule(decoderConfig); // eslint-disable-line no-undef - }); - break; - - case "decode": - const buffer = message.buffer; - const taskConfig = message.taskConfig; - decoderPending.then((module) => { - const draco = module.draco; - const decoder = new draco.Decoder(); - const decoderBuffer = new draco.DecoderBuffer(); - decoderBuffer.Init(new Int8Array(buffer), buffer.byteLength); - - try { - const geometry = decodeGeometry(draco, decoder, decoderBuffer, taskConfig); - const buffers = geometry.attributes.map((attr) => attr.array.buffer); - if (geometry.index) buffers.push(geometry.index.array.buffer); - self.postMessage( - { - type: "decode", - id: message.id, - geometry, - }, - buffers, - ); - } catch (error) { - console.error(error); - self.postMessage({ - type: "error", - id: message.id, - error: error.message, - }); - } finally { - draco.destroy(decoderBuffer); - draco.destroy(decoder); - } - }); - break; - } - }; - - function decodeGeometry(draco, decoder, decoderBuffer, taskConfig) { - const attributeIDs = taskConfig.attributeIDs; - const attributeTypes = taskConfig.attributeTypes; - let dracoGeometry; - let decodingStatus; - const geometryType = decoder.GetEncodedGeometryType(decoderBuffer); - - if (geometryType === draco.TRIANGULAR_MESH) { - dracoGeometry = new draco.Mesh(); - decodingStatus = decoder.DecodeBufferToMesh(decoderBuffer, dracoGeometry); - } else if (geometryType === draco.POINT_CLOUD) { - dracoGeometry = new draco.PointCloud(); - decodingStatus = decoder.DecodeBufferToPointCloud(decoderBuffer, dracoGeometry); - } else { - throw new Error("THREE.DRACOLoader: Unexpected geometry type."); - } - - if (!decodingStatus.ok() || dracoGeometry.ptr === 0) { - throw new Error("THREE.DRACOLoader: Decoding failed: " + decodingStatus.error_msg()); - } - - const geometry = { - index: null, - attributes: [], - }; // Gather all vertex attributes. - - for (const attributeName in attributeIDs) { - const attributeType = self[attributeTypes[attributeName]]; - let attribute; - let attributeID; // A Draco file may be created with default vertex attributes, whose attribute IDs - // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively, - // a Draco file may contain a custom set of attributes, identified by known unique - // IDs. glTF files always do the latter, and `.drc` files typically do the former. - - if (taskConfig.useUniqueIDs) { - attributeID = attributeIDs[attributeName]; - attribute = decoder.GetAttributeByUniqueId(dracoGeometry, attributeID); - } else { - attributeID = decoder.GetAttributeId(dracoGeometry, draco[attributeIDs[attributeName]]); - if (attributeID === -1) continue; - attribute = decoder.GetAttribute(dracoGeometry, attributeID); - } - - geometry.attributes.push( - decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute), - ); - } // Add index. - - if (geometryType === draco.TRIANGULAR_MESH) { - geometry.index = decodeIndex(draco, decoder, dracoGeometry); - } - - draco.destroy(dracoGeometry); - return geometry; - } - - function decodeIndex(draco, decoder, dracoGeometry) { - const numFaces = dracoGeometry.num_faces(); - const numIndices = numFaces * 3; - const byteLength = numIndices * 4; - - const ptr = draco._malloc(byteLength); - - decoder.GetTrianglesUInt32Array(dracoGeometry, byteLength, ptr); - const index = new Uint32Array(draco.HEAPF32.buffer, ptr, numIndices).slice(); - - draco._free(ptr); - - return { - array: index, - itemSize: 1, - }; - } - - function decodeAttribute( - draco, - decoder, - dracoGeometry, - attributeName, - attributeType, - attribute, - ) { - const numComponents = attribute.num_components(); - const numPoints = dracoGeometry.num_points(); - const numValues = numPoints * numComponents; - const byteLength = numValues * attributeType.BYTES_PER_ELEMENT; - const dataType = getDracoDataType(draco, attributeType); - - const ptr = draco._malloc(byteLength); - - decoder.GetAttributeDataArrayForAllPoints( - dracoGeometry, - attribute, - dataType, - byteLength, - ptr, - ); - const array = new attributeType(draco.HEAPF32.buffer, ptr, numValues).slice(); - - draco._free(ptr); - - return { - name: attributeName, - array: array, - itemSize: numComponents, - }; - } - - function getDracoDataType(draco, attributeType) { - switch (attributeType) { - case Float32Array: - return draco.DT_FLOAT32; - - case Int8Array: - return draco.DT_INT8; - - case Int16Array: - return draco.DT_INT16; - - case Int32Array: - return draco.DT_INT32; - - case Uint8Array: - return draco.DT_UINT8; - - case Uint16Array: - return draco.DT_UINT16; - - case Uint32Array: - return draco.DT_UINT32; - } - } - } - - THREE.DRACOLoader = DRACOLoader; -})(); diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 9959fd6adac..f2725d561e4 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -4,7 +4,7 @@ import { V3 } from "libs/mjs"; import { sleep } from "libs/utils"; import ErrorHandling from "libs/error_handling"; import type { APIDataLayer } from "types/api_flow_types"; -import "libs/DRACOLoader.js"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; import { ResolutionInfo, @@ -599,7 +599,7 @@ function* loadPrecomputedMeshForSegmentId( ); const loader = getDracoLoader(); - const geometry = yield* call(loader.decodeDracoFile.bind(loader), dracoData); + const geometry = yield* call(loader.decodeDracoFileAsync, dracoData); yield* call( { context: sceneController, fn: sceneController.addIsosurfaceFromGeometry }, geometry, @@ -644,13 +644,25 @@ function getDracoLoader() { if (_dracoLoader) { return _dracoLoader; } - _dracoLoader = new THREE.DRACOLoader(); + _dracoLoader = new DRACOLoader(); _dracoLoader.setDecoderPath( "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/", ); _dracoLoader.setDecoderConfig({ type: "js" }); + _dracoLoader.decodeDracoFileAsync = (buffer, ...args) => + new Promise((resolve, reject) => + _dracoLoader.decodeDracoFile( + buffer, + (x) => { + debugger; + resolve(x); + }, + ...args, + ), + ); + // todo: never dispose? // loader.dispose(); diff --git a/public/draco_file.bin b/public/draco_file.bin deleted file mode 100644 index 96ec360ddbfef72332e9fde2f3bd34fa2ca6357d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 251 zcmV)B3}%UkOd?Jr8rgk~f~*4|bojJJO?w9xUW0^R z3@`#C*tQ#hN(6s|!VKUE+ChjrKt6sRiT;SkGr%vQ0OT2P z4>WH*xZ&j)8oLrO(-Mej0RZS9k%A#c*ra}V%gCn5Vd+#I4)}zbCxAUIlr|Fp5#K=r zD)LgeR*z(>1BSso@%I7L1|#h_yQgXe000000RIC30I#64LN8J}LwdNFLtsh!K?(<^ BTtNT; From d506a52feeaf0981a7314aa84d1becdb82717297 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 16:58:52 +0200 Subject: [PATCH 24/63] use wasm for draco decoding --- frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index f2725d561e4..2c865e28eaf 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -649,7 +649,7 @@ function getDracoLoader() { _dracoLoader.setDecoderPath( "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/", ); - _dracoLoader.setDecoderConfig({ type: "js" }); + _dracoLoader.setDecoderConfig({ type: "wasm" }); _dracoLoader.decodeDracoFileAsync = (buffer, ...args) => new Promise((resolve, reject) => From dbdd895e8be91621a5966f6e1ba9260f9fd72599 Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 29 Sep 2022 17:10:57 +0200 Subject: [PATCH 25/63] reformat and rename fragment to chunk --- .../datastore/services/MeshFileService.scala | 84 +++++++++---------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index bc87fcfce21..083c4b93c55 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -68,17 +68,17 @@ case class NeuroglancerSegmentInfo(chunkShape: Vec3Float, numLods: Int, lodScales: Array[Float], vertexOffsets: Array[Vec3Float], - numFragmentsPerLod: Array[Int], - fragmentPositions: List[List[Vec3Int]], - fragmentOffsets: List[List[Int]]) + numChunksPerLod: Array[Int], + chunkPositions: List[List[Vec3Int]], + chunkByteOffsets: List[List[Int]]) // TODO position als Vec3Int -case class MeshFragment(position: Vec3Float, byteOffset: Int, byteSize: Int) +case class MeshChunk(position: Vec3Float, byteOffset: Int, byteSize: Int) -object MeshFragment { - implicit val jsonFormat: OFormat[MeshFragment] = Json.format[MeshFragment] +object MeshChunk { + implicit val jsonFormat: OFormat[MeshChunk] = Json.format[MeshChunk] } -case class MeshLodInfo(scale: Int, vertexOffset: Vec3Float, chunkShape: Vec3Float, fragments: List[MeshFragment]) +case class MeshLodInfo(scale: Int, vertexOffset: Vec3Float, chunkShape: Vec3Float, chunks: List[MeshChunk]) object MeshLodInfo { implicit val jsonFormat: OFormat[MeshLodInfo] = Json.format[MeshLodInfo] @@ -210,36 +210,36 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .uint8() .readArrayBlockWithOffset("/neuroglancer", (neuroglancerEnd - neuroglancerStart).toInt, neuroglancerStart) val segmentInfo = parseNeuroglancerManifest(manifest) - val meshfile = getFragmentsFromSegmentInfo(segmentInfo, lodScaleMultiplier, neuroglancerStart) + val meshfile = getChunksFromSegmentInfo(segmentInfo, lodScaleMultiplier, neuroglancerStart) WebknossosSegmentInfo(transform = transform, meshFormat = encoding, chunks = meshfile) } } - private def getFragmentsFromSegmentInfo(segmentInfo: NeuroglancerSegmentInfo, - lodScaleMultiplier: Double, - neuroglancerOffsetStart: Long): MeshSegmentInfo = { - val totalMeshSize = segmentInfo.fragmentOffsets.map(_.sum).sum + private def getChunksFromSegmentInfo(segmentInfo: NeuroglancerSegmentInfo, + lodScaleMultiplier: Double, + neuroglancerOffsetStart: Long): MeshSegmentInfo = { + val totalMeshSize = segmentInfo.chunkByteOffsets.map(_.sum).sum val meshByteStartOffset = neuroglancerOffsetStart - totalMeshSize - val fragmentByteOffsets = segmentInfo.fragmentOffsets.map(_.scanLeft(0)(_ + _)) // This builds a cumulative sum + val chunkByteOffsets = segmentInfo.chunkByteOffsets.map(_.scanLeft(0)(_ + _)) // This builds a cumulative sum - def computeGlobalPositionAndOffset(lod: Int, currentFragment: Int): MeshFragment = { + def computeGlobalPositionAndOffset(lod: Int, currentChunk: Int): MeshChunk = { val globalPosition = segmentInfo.gridOrigin + segmentInfo - .fragmentPositions(lod)(currentFragment) + .chunkPositions(lod)(currentChunk) .toVec3Float * segmentInfo.chunkShape * segmentInfo.lodScales(lod) * lodScaleMultiplier - MeshFragment( + MeshChunk( position = globalPosition, // This position is in Voxel Space - byteOffset = meshByteStartOffset.toInt + fragmentByteOffsets(lod)(currentFragment), - byteSize = segmentInfo.fragmentOffsets(lod)(currentFragment), + byteOffset = meshByteStartOffset.toInt + chunkByteOffsets(lod)(currentChunk), + byteSize = segmentInfo.chunkByteOffsets(lod)(currentChunk), ) } val lods = for (lod <- 0 until segmentInfo.numLods) yield lod - def fragmentNums(lod: Int): IndexedSeq[(Int, Int)] = - for (currentFragment <- 0 until segmentInfo.numFragmentsPerLod(lod)) - yield (lod, currentFragment) - val fragments = lods.map(lod => fragmentNums(lod).map(x => computeGlobalPositionAndOffset(x._1, x._2)).toList) + def chunkNums(lod: Int): IndexedSeq[(Int, Int)] = + for (currentChunk <- 0 until segmentInfo.numChunksPerLod(lod)) + yield (lod, currentChunk) + val chunks = lods.map(lod => chunkNums(lod).map(x => computeGlobalPositionAndOffset(x._1, x._2)).toList) val meshfileLods = lods .map( @@ -247,7 +247,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC MeshLodInfo(scale = segmentInfo.lodScales(lod).toInt, vertexOffset = segmentInfo.vertexOffsets(lod), chunkShape = segmentInfo.chunkShape, - fragments = fragments(lod))) + chunks = chunks(lod))) .toList MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods) } @@ -273,30 +273,30 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC vertexOffsets(d) = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) } - val numFragmentsPerLod = new Array[Int](numLods) + val numChunksPerLod = new Array[Int](numLods) for (lod <- 0 until numLods) { - numFragmentsPerLod(lod) = dis.readInt() + numChunksPerLod(lod) = dis.readInt() } - val fragmentPositionsList = new ListBuffer[List[Vec3Int]] - val fragmentSizes = new ListBuffer[List[Int]] + val chunkPositionsList = new ListBuffer[List[Vec3Int]] + val chunkSizes = new ListBuffer[List[Int]] for (lod <- 0 until numLods) { - val currentFragmentPositions = (ListBuffer[Int](), ListBuffer[Int](), ListBuffer[Int]()) - for (row <- 0 until 3; _ <- 0 until numFragmentsPerLod(lod)) { + val currentChunkPositions = (ListBuffer[Int](), ListBuffer[Int](), ListBuffer[Int]()) + for (row <- 0 until 3; _ <- 0 until numChunksPerLod(lod)) { row match { - case 0 => currentFragmentPositions._1.append(dis.readInt) - case 1 => currentFragmentPositions._2.append(dis.readInt) - case 2 => currentFragmentPositions._3.append(dis.readInt) + case 0 => currentChunkPositions._1.append(dis.readInt) + case 1 => currentChunkPositions._2.append(dis.readInt) + case 2 => currentChunkPositions._3.append(dis.readInt) } } - fragmentPositionsList.append(currentFragmentPositions.zipped.map(Vec3Int(_, _, _)).toList) + chunkPositionsList.append(currentChunkPositions.zipped.map(Vec3Int(_, _, _)).toList) - val currentFragmentSizes = ListBuffer[Int]() - for (_ <- 0 until numFragmentsPerLod(lod)) { - currentFragmentSizes.append(dis.readInt) + val currentChunkSizes = ListBuffer[Int]() + for (_ <- 0 until numChunksPerLod(lod)) { + currentChunkSizes.append(dis.readInt) } - fragmentSizes.append(currentFragmentSizes.toList) + chunkSizes.append(currentChunkSizes.toList) } NeuroglancerSegmentInfo(chunkShape, @@ -304,9 +304,9 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC numLods, lodScales, vertexOffsets, - numFragmentsPerLod, - fragmentPositionsList.toList, - fragmentSizes.toList) + numChunksPerLod, + chunkPositionsList.toList, + chunkSizes.toList) } private def getNeuroglancerOffsets(segmentId: Long, cachedMeshFile: CachedHdf5File): (Long, Long) = { @@ -366,9 +366,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val data = cachedMeshFile.reader .uint8() - .readArrayBlockWithOffset("neuroglancer", - meshChunkDataRequest.byteSize, - meshChunkDataRequest.byteOffset) + .readArrayBlockWithOffset("neuroglancer", meshChunkDataRequest.byteSize, meshChunkDataRequest.byteOffset) (data, meshFormat) } ?~> "mesh.file.readData.failed" } From 56ba4e2574267b3240a71a13c3c8a2b4552a7c59 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 17:47:36 +0200 Subject: [PATCH 26/63] clean up typing for draco loader --- .eslintrc.json | 1 + frontend/javascripts/libs/draco.ts | 34 +++++++++++++++++++ frontend/javascripts/oxalis/api/api_latest.ts | 2 +- .../oxalis/model/sagas/isosurface_saga.ts | 33 +----------------- .../oxalis/view/action_bar_view.tsx | 2 +- .../segments_tab/segments_view_helper.tsx | 2 +- 6 files changed, 39 insertions(+), 35 deletions(-) create mode 100644 frontend/javascripts/libs/draco.ts diff --git a/.eslintrc.json b/.eslintrc.json index 6faa372de9f..61974196b5b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -40,6 +40,7 @@ "eslint-comments/no-unused-disable": "error", "flowtype/no-types-missing-file-annotation": "off", "import/extensions": ["warn", { "js": "never", "jsx": "always" }], + "import/prefer-default-export": ["warn"], "import/no-cycle": "off", "import/no-extraneous-dependencies": [ "error", diff --git a/frontend/javascripts/libs/draco.ts b/frontend/javascripts/libs/draco.ts new file mode 100644 index 00000000000..e0af0bc3b43 --- /dev/null +++ b/frontend/javascripts/libs/draco.ts @@ -0,0 +1,34 @@ +import { BufferGeometry } from "three"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; + +let _dracoLoader: CustomDRACOLoader | null; + +class CustomDRACOLoader extends DRACOLoader { + // Subclass to create a promise-based API and add typing + decodeDracoFileAsync = (buffer: ArrayBuffer, ...args: any[]): Promise => + new Promise((resolve) => { + if (_dracoLoader == null) { + throw new Error("DracoLoader not instantiated."); + } + // @ts-ignore + _dracoLoader.decodeDracoFile(buffer, resolve, ...args); + }); +} + +export function getDracoLoader(): CustomDRACOLoader { + if (_dracoLoader) { + return _dracoLoader; + } + _dracoLoader = new CustomDRACOLoader(); + + _dracoLoader.setDecoderPath( + "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/", + ); + _dracoLoader.setDecoderConfig({ type: "wasm" }); + // The loader could theoretically be disposed like this: + // _dracoLoader.dispose(); + // However, it's probably okay to not release the resources, + // since the loader might be used again soon, anyway. + + return _dracoLoader; +} diff --git a/frontend/javascripts/oxalis/api/api_latest.ts b/frontend/javascripts/oxalis/api/api_latest.ts index 3bafa75c9dc..38be03d21fa 100644 --- a/frontend/javascripts/oxalis/api/api_latest.ts +++ b/frontend/javascripts/oxalis/api/api_latest.ts @@ -1842,7 +1842,7 @@ class DataApi { const { mappingName, meshFileName } = currentMeshFile; // todo: remove second condition - if (mappingName != null && mappingName != "") { + if (mappingName != null && mappingName !== "") { const activeMapping = this.getActiveMapping(effectiveLayerName); if (mappingName !== activeMapping) { diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 2c865e28eaf..5c64a27b872 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -4,7 +4,6 @@ import { V3 } from "libs/mjs"; import { sleep } from "libs/utils"; import ErrorHandling from "libs/error_handling"; import type { APIDataLayer } from "types/api_flow_types"; -import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; import { ResolutionInfo, @@ -53,11 +52,11 @@ import window from "libs/window"; import { getActiveSegmentationTracing } from "oxalis/model/accessors/volumetracing_accessor"; import { saveNowAction } from "oxalis/model/actions/save_actions"; import Toast from "libs/toast"; +import { getDracoLoader } from "libs/draco"; import messages from "messages"; import processTaskWithPool from "libs/task_pool"; import { getBaseSegmentationName } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { UpdateSegmentAction } from "../actions/volumetracing_actions"; -import * as THREE from "three"; const MAX_RETRY_COUNT = 5; const RETRY_WAIT_TIME = 5000; @@ -639,36 +638,6 @@ function* loadPrecomputedMeshForSegmentId( yield* put(finishedLoadingIsosurfaceAction(layerName, id)); } -let _dracoLoader; -function getDracoLoader() { - if (_dracoLoader) { - return _dracoLoader; - } - _dracoLoader = new DRACOLoader(); - - _dracoLoader.setDecoderPath( - "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/", - ); - _dracoLoader.setDecoderConfig({ type: "wasm" }); - - _dracoLoader.decodeDracoFileAsync = (buffer, ...args) => - new Promise((resolve, reject) => - _dracoLoader.decodeDracoFile( - buffer, - (x) => { - debugger; - resolve(x); - }, - ...args, - ), - ); - - // todo: never dispose? - // loader.dispose(); - - return _dracoLoader; -} - /* * * Ad Hoc and Precomputed Meshes diff --git a/frontend/javascripts/oxalis/view/action_bar_view.tsx b/frontend/javascripts/oxalis/view/action_bar_view.tsx index 11a6ec0519e..36d544dc4a7 100644 --- a/frontend/javascripts/oxalis/view/action_bar_view.tsx +++ b/frontend/javascripts/oxalis/view/action_bar_view.tsx @@ -28,7 +28,7 @@ import { getVisibleSegmentationLayer, } from "oxalis/model/accessors/dataset_accessor"; import { AsyncButton } from "components/async_clickables"; -import ButtonComponent from "./components/button_component"; + const VersionRestoreWarning = ( > mappingName === undefined || layerName == null || mappingName === enabledMappingName || - mappingName == "" + mappingName === "" ) { // @ts-expect-error ts-migrate(2322) FIXME: Type 'Omit, ... Remove this comment to see the full error message return ; From 44951071b9e1ff69fa8fd77d36d5344c9aaa6aba Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 17:48:31 +0200 Subject: [PATCH 27/63] remove unused draco3d image --- package.json | 3 +-- yarn.lock | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/package.json b/package.json index 58e2bf943bb..e4fa20735aa 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,6 @@ "deep-freeze": "0.0.1", "dice-coefficient": "^2.1.0", "distance-transform": "^1.0.2", - "draco3d": "^1.5.3", "eslint-plugin-ava": "^13.2.0", "file-saver": "^2.0.1", "flexlayout-react": "^0.5.5", @@ -201,8 +200,8 @@ "process": "^0.11.10", "protobufjs": "^6.10.3", "react": "^16.12.0", - "react-colorful": "^5.6.1", "react-ansi": "^3.0.2", + "react-colorful": "^5.6.1", "react-debounce-render": "^8.0.1", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", diff --git a/yarn.lock b/yarn.lock index 1c647e424c4..19687b5ca92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5540,11 +5540,6 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -draco3d@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/draco3d/-/draco3d-1.5.3.tgz#75dfb3da7d1420571b1ab999191c49fdc2a74571" - integrity sha512-Ahum6SewAd1oVMm6Fk8T/zCE0qbzjohhO5pl1Xp5Outl4JKv7jYicfd5vNtkzImx94XE35fhNXVqHk9ajt+6Tg== - draft-js@^0.10.0, draft-js@~0.10.0: version "0.10.5" resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.10.5.tgz#bfa9beb018fe0533dbb08d6675c371a6b08fa742" From 39596a1cfa924bbb8a86684ac70de4e69183df8b Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 17:50:19 +0200 Subject: [PATCH 28/63] pin URI to WASM in GH repo --- frontend/javascripts/libs/draco.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/libs/draco.ts b/frontend/javascripts/libs/draco.ts index e0af0bc3b43..f464f632db5 100644 --- a/frontend/javascripts/libs/draco.ts +++ b/frontend/javascripts/libs/draco.ts @@ -22,7 +22,7 @@ export function getDracoLoader(): CustomDRACOLoader { _dracoLoader = new CustomDRACOLoader(); _dracoLoader.setDecoderPath( - "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/", + "https://raw.githubusercontent.com/mrdoob/three.js/r145/examples/js/libs/draco/", ); _dracoLoader.setDecoderConfig({ type: "wasm" }); // The loader could theoretically be disposed like this: From 97facd43f505c9ec9cdaf1b9e8a498c91ab4457a Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 17:54:33 +0200 Subject: [PATCH 29/63] rename fragment to chunk like the back-end did it --- frontend/javascripts/admin/api/mesh_v3.ts | 4 ++-- frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/admin/api/mesh_v3.ts b/frontend/javascripts/admin/api/mesh_v3.ts index 0cb80696b83..db3ae0c0537 100644 --- a/frontend/javascripts/admin/api/mesh_v3.ts +++ b/frontend/javascripts/admin/api/mesh_v3.ts @@ -3,13 +3,13 @@ import { Vector3 } from "oxalis/constants"; import { APIDatasetId } from "types/api_flow_types"; import { doWithToken } from "./token"; -export type MeshFragment = { position: Vector3; byteOffset: number; byteSize: number }; +export type MeshChunk = { position: Vector3; byteOffset: number; byteSize: number }; type MeshLodInfo = { scale: number; vertexOffset: Vector3; chunkShape: Vector3; - fragments: Array; + chunks: Array; }; type MeshSegmentInfo = { diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 5c64a27b872..77760fe9058 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -557,7 +557,7 @@ function* loadPrecomputedMeshForSegmentId( meshFileName, id, ); - availableChunks = _.first(segmentInfo.chunks.lods)?.fragments || []; + availableChunks = _.first(segmentInfo.chunks.lods)?.chunks || []; } else { availableChunks = yield* call( meshV0.getMeshfileChunksForSegment, @@ -577,9 +577,9 @@ function* loadPrecomputedMeshForSegmentId( } // Sort the chunks by distance to the seedPosition, so that the mesh loads from the inside out - const sortedAvailableChunks = _.sortBy(availableChunks, (chunk: Vector3 | meshV3.MeshFragment) => + const sortedAvailableChunks = _.sortBy(availableChunks, (chunk: Vector3 | meshV3.MeshChunk) => V3.length(V3.sub(seedPosition, "position" in chunk ? chunk.position : chunk)), - ) as Array | Array; + ) as Array | Array; const tasks = sortedAvailableChunks.map( (chunk) => From 7b7764a1943b9f636655c9a812d2410db383bd9a Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 29 Sep 2022 18:29:57 +0200 Subject: [PATCH 30/63] NOW ALL NEW: THE GREAT NEW MESHCONTROLLER --- .../controllers/DataSourceController.scala | 107 --------------- .../controllers/MeshController.scala | 125 ++++++++++++++++++ .../datastore/services/MeshFileService.scala | 5 - ....scalableminds.webknossos.datastore.routes | 11 +- 4 files changed, 130 insertions(+), 118 deletions(-) create mode 100644 webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala index b400eea832b..958188dc1cf 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala @@ -29,7 +29,6 @@ class DataSourceController @Inject()( remoteWebKnossosClient: DSRemoteWebKnossosClient, accessTokenService: DataStoreAccessTokenService, binaryDataServiceHolder: BinaryDataServiceHolder, - meshFileService: MeshFileService, connectomeFileService: ConnectomeFileService, uploadService: UploadService )(implicit bodyParsers: PlayBodyParsers) @@ -400,112 +399,6 @@ Expects: } } - def dummyDracoFile(): Action[AnyContent] = Action.async { implicit request => - for { - draco <- meshFileService.readDummyDraco() - } yield Ok(draco) - } - - @ApiOperation(hidden = true, value = "") - def listMeshFiles(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String): Action[AnyContent] = - Action.async { implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), - urlOrHeaderToken(token, request)) { - for { - meshFiles <- meshFileService.exploreMeshFiles(organizationName, dataSetName, dataLayerName) - } yield Ok(Json.toJson(meshFiles)) - } - } - - @ApiOperation(hidden = true, value = "") - def listMeshChunksForSegmentV0(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String): Action[ListMeshChunksRequest] = - Action.async(validateJson[ListMeshChunksRequest]) { implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), - urlOrHeaderToken(token, request)) { - for { - positions <- meshFileService.listMeshChunksForSegmentV0(organizationName, - dataSetName, - dataLayerName, - request.body) ?~> Messages( - "mesh.file.listChunks.failed", - request.body.segmentId.toString, - request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST - } yield Ok(Json.toJson(positions)) - } - } - - @ApiOperation(hidden = true, value = "") - def listMeshChunksForSegmentForVersion(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String, - formatVersion: Int): Action[ListMeshChunksRequest] = - Action.async(validateJson[ListMeshChunksRequest]) { implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), - urlOrHeaderToken(token, request)) { - for { - positions <- formatVersion match { - case 3 => - meshFileService.listMeshChunksForSegmentV3(organizationName, dataSetName, dataLayerName, request.body) ?~> Messages( - "mesh.file.listChunks.failed", - request.body.segmentId.toString, - request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST - case _ => Fox.failure("Wrong format version") ~> BAD_REQUEST - } - } yield Ok(Json.toJson(positions)) - } - } - - @ApiOperation(hidden = true, value = "") - def readMeshChunkV0(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String): Action[MeshChunkDataRequestV0] = - Action.async(validateJson[MeshChunkDataRequestV0]) { implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), - urlOrHeaderToken(token, request)) { - for { - (data, encoding) <- meshFileService.readMeshChunkV0(organizationName, - dataSetName, - dataLayerName, - request.body) ?~> "mesh.file.loadChunk.failed" - } yield { - if (encoding.contains("gzip")) { - Ok(data).withHeaders("Content-Encoding" -> "gzip") - } else Ok(data) - } - } - } - - @ApiOperation(hidden = true, value = "") - def readMeshChunkForVersion(token: Option[String], - organizationName: String, - dataSetName: String, - dataLayerName: String, - formatVersion: Int): Action[MeshChunkDataRequestV3] = - Action.async(validateJson[MeshChunkDataRequestV3]) { implicit request => - accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), - urlOrHeaderToken(token, request)) { - for { - (data, encoding) <- formatVersion match { - case 3 => - meshFileService.readMeshChunkV3(organizationName, dataSetName, dataLayerName, request.body) ?~> "mesh.file.loadChunk.failed" - case _ => Fox.failure("Wrong format version") ~> BAD_REQUEST - } - } yield { - if (encoding.contains("gzip")) { - Ok(data).withHeaders("Content-Encoding" -> "gzip") - } else Ok(data) - } - } - } - @ApiOperation(hidden = true, value = "") def update(token: Option[String], organizationName: String, dataSetName: String): Action[DataSource] = Action.async(validateJson[DataSource]) { implicit request => diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala new file mode 100644 index 00000000000..c2e9131932d --- /dev/null +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala @@ -0,0 +1,125 @@ +package com.scalableminds.webknossos.datastore.controllers + +import com.google.inject.Inject +import com.scalableminds.util.tools.{Fox, FoxImplicits} +import com.scalableminds.webknossos.datastore.controllers.Controller +import com.scalableminds.webknossos.datastore.models.datasource.DataSourceId +import com.scalableminds.webknossos.datastore.services._ +import play.api.i18n.Messages +import play.api.libs.json.Json +import play.api.mvc.{Action, AnyContent, PlayBodyParsers} + +import io.swagger.annotations.{Api, ApiOperation} + +import scala.concurrent.ExecutionContext.Implicits.global + +@Api(tags = Array("datastore")) +class MeshController @Inject()( + accessTokenService: DataStoreAccessTokenService, + meshFileService: MeshFileService, +)(implicit bodyParsers: PlayBodyParsers) + extends Controller + with FoxImplicits { + + override def allowRemoteOrigin: Boolean = true + + @ApiOperation(hidden = true, value = "") + def listMeshFiles(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String): Action[AnyContent] = + Action.async { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), + urlOrHeaderToken(token, request)) { + for { + meshFiles <- meshFileService.exploreMeshFiles(organizationName, dataSetName, dataLayerName) + } yield Ok(Json.toJson(meshFiles)) + } + } + + @ApiOperation(hidden = true, value = "") + def listMeshChunksForSegmentV0(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String): Action[ListMeshChunksRequest] = + Action.async(validateJson[ListMeshChunksRequest]) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), + urlOrHeaderToken(token, request)) { + for { + positions <- meshFileService.listMeshChunksForSegmentV0(organizationName, + dataSetName, + dataLayerName, + request.body) ?~> Messages( + "mesh.file.listChunks.failed", + request.body.segmentId.toString, + request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST + } yield Ok(Json.toJson(positions)) + } + } + + @ApiOperation(hidden = true, value = "") + def listMeshChunksForSegmentForVersion(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String, + formatVersion: Int): Action[ListMeshChunksRequest] = + Action.async(validateJson[ListMeshChunksRequest]) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), + urlOrHeaderToken(token, request)) { + for { + positions <- formatVersion match { + case 3 => + meshFileService.listMeshChunksForSegmentV3(organizationName, dataSetName, dataLayerName, request.body) ?~> Messages( + "mesh.file.listChunks.failed", + request.body.segmentId.toString, + request.body.meshFile) ?~> Messages("mesh.file.load.failed", request.body.segmentId.toString) ~> BAD_REQUEST + case _ => Fox.failure("Wrong format version") ~> BAD_REQUEST + } + } yield Ok(Json.toJson(positions)) + } + } + + @ApiOperation(hidden = true, value = "") + def readMeshChunkV0(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String): Action[MeshChunkDataRequestV0] = + Action.async(validateJson[MeshChunkDataRequestV0]) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), + urlOrHeaderToken(token, request)) { + for { + (data, encoding) <- meshFileService.readMeshChunkV0(organizationName, + dataSetName, + dataLayerName, + request.body) ?~> "mesh.file.loadChunk.failed" + } yield { + if (encoding.contains("gzip")) { + Ok(data).withHeaders("Content-Encoding" -> "gzip") + } else Ok(data) + } + } + } + + @ApiOperation(hidden = true, value = "") + def readMeshChunkForVersion(token: Option[String], + organizationName: String, + dataSetName: String, + dataLayerName: String, + formatVersion: Int): Action[MeshChunkDataRequestV3] = + Action.async(validateJson[MeshChunkDataRequestV3]) { implicit request => + accessTokenService.validateAccess(UserAccessRequest.readDataSources(DataSourceId(dataSetName, organizationName)), + urlOrHeaderToken(token, request)) { + for { + (data, encoding) <- formatVersion match { + case 3 => + meshFileService.readMeshChunkV3(organizationName, dataSetName, dataLayerName, request.body) ?~> "mesh.file.loadChunk.failed" + case _ => Fox.failure("Wrong format version") ~> BAD_REQUEST + } + } yield { + if (encoding.contains("gzip")) { + Ok(data).withHeaders("Content-Encoding" -> "gzip") + } else Ok(data) + } + } + } +} diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 083c4b93c55..881dd13ed3c 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -111,11 +111,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC private lazy val meshFileCache = new Hdf5FileCache(30) - def readDummyDraco(): Fox[Array[Byte]] = { - val path = dataBaseDir.resolve("draco_file.bin") - tryo(Files.readAllBytes(path)).toOption.toFox - } - def exploreMeshFiles(organizationName: String, dataSetName: String, dataLayerName: String): Fox[Set[MeshFileInfo]] = { val layerDir = dataBaseDir.resolve(organizationName).resolve(dataSetName).resolve(dataLayerName) val meshFileNames = PathUtils diff --git a/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes b/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes index c647910ae95..4efb8744b76 100644 --- a/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes +++ b/webknossos-datastore/conf/com.scalableminds.webknossos.datastore.routes @@ -55,12 +55,11 @@ POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/agg # Mesh files -GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/formatVersion/:formatVersion/chunks @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listMeshChunksForSegmentForVersion(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, formatVersion: Int) -POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/formatVersion/:formatVersion/chunks/data @com.scalableminds.webknossos.datastore.controllers.DataSourceController.readMeshChunkForVersion(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, formatVersion: Int) -GET /datasets/dummyDraco @com.scalableminds.webknossos.datastore.controllers.DataSourceController.dummyDracoFile() +GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes @com.scalableminds.webknossos.datastore.controllers.MeshController.listMeshFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks @com.scalableminds.webknossos.datastore.controllers.MeshController.listMeshChunksForSegmentV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/chunks/data @com.scalableminds.webknossos.datastore.controllers.MeshController.readMeshChunkV0(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/formatVersion/:formatVersion/chunks @com.scalableminds.webknossos.datastore.controllers.MeshController.listMeshChunksForSegmentForVersion(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, formatVersion: Int) +POST /datasets/:organizationName/:dataSetName/layers/:dataLayerName/meshes/formatVersion/:formatVersion/chunks/data @com.scalableminds.webknossos.datastore.controllers.MeshController.readMeshChunkForVersion(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String, formatVersion: Int) # Connectome files GET /datasets/:organizationName/:dataSetName/layers/:dataLayerName/connectomes @com.scalableminds.webknossos.datastore.controllers.DataSourceController.listConnectomeFiles(token: Option[String], organizationName: String, dataSetName: String, dataLayerName: String) From e9f26c66c77efd3208cc6d853367d191597a27c9 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 29 Sep 2022 19:01:31 +0200 Subject: [PATCH 31/63] fix colors by computing vertex normals --- frontend/javascripts/oxalis/controller/scene_controller.ts | 3 ++- frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 904f7b095d2..d7074f48d18 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -200,8 +200,9 @@ class SceneController { constructIsosurfaceMesh(cellId: number, geometry: THREE.BufferGeometry, passive: boolean) { const color = this.getColorObjectForSegment(cellId); - const meshMaterial = new THREE.MeshLambertMaterial({ + const meshMaterial = new THREE.MeshStandardMaterial({ color, + // flatShading: true, }); meshMaterial.side = THREE.FrontSide; meshMaterial.transparent = true; diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 77760fe9058..62c49df9b4a 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -546,7 +546,9 @@ function* loadPrecomputedMeshForSegmentId( const dataset = yield* select((state) => state.dataset); let availableChunks = null; - const version = 3; + + // todo: dont hardcode + const version = meshFileName.includes("4-4") ? 0 : 3; try { if (version === 3) { const segmentInfo = yield* call( @@ -599,6 +601,8 @@ function* loadPrecomputedMeshForSegmentId( const loader = getDracoLoader(); const geometry = yield* call(loader.decodeDracoFileAsync, dracoData); + geometry.computeVertexNormals(); + yield* call( { context: sceneController, fn: sceneController.addIsosurfaceFromGeometry }, geometry, From 233f03e7e0937ff18efb7f02c9c1c04e19221573 Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 29 Sep 2022 19:14:41 +0200 Subject: [PATCH 32/63] remove todo --- .../webknossos/datastore/services/MeshFileService.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 881dd13ed3c..b4b597ba659 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -72,7 +72,6 @@ case class NeuroglancerSegmentInfo(chunkShape: Vec3Float, chunkPositions: List[List[Vec3Int]], chunkByteOffsets: List[List[Int]]) -// TODO position als Vec3Int case class MeshChunk(position: Vec3Float, byteOffset: Int, byteSize: Int) object MeshChunk { From 562da591be31c1826f7807a1a5a8db977f2b335d Mon Sep 17 00:00:00 2001 From: leowe Date: Thu, 29 Sep 2022 19:44:59 +0200 Subject: [PATCH 33/63] what we don't need we don't keep --- .../webknossos/datastore/controllers/MeshController.scala | 1 - .../webknossos/datastore/services/MeshFileService.scala | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala index c2e9131932d..ad05c79ac87 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala @@ -2,7 +2,6 @@ package com.scalableminds.webknossos.datastore.controllers import com.google.inject.Inject import com.scalableminds.util.tools.{Fox, FoxImplicits} -import com.scalableminds.webknossos.datastore.controllers.Controller import com.scalableminds.webknossos.datastore.models.datasource.DataSourceId import com.scalableminds.webknossos.datastore.services._ import play.api.i18n.Messages diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index b4b597ba659..290d0f0947e 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -16,7 +16,7 @@ import play.api.libs.json.{Json, OFormat} import java.io.ByteArrayInputStream import java.nio.ByteBuffer -import java.nio.file.{Files, Path, Paths} +import java.nio.file.{Path, Paths} import javax.inject.Inject import scala.collection.JavaConverters._ import scala.collection.mutable.ListBuffer From 5c71d4bdf454d8927196ac79f7a30594b305130b Mon Sep 17 00:00:00 2001 From: leowe <13684843+leowe@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:47:11 +0200 Subject: [PATCH 34/63] Update util/src/main/scala/com/scalableminds/util/tools/Fox.scala Co-authored-by: Florian M --- util/src/main/scala/com/scalableminds/util/tools/Fox.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/src/main/scala/com/scalableminds/util/tools/Fox.scala b/util/src/main/scala/com/scalableminds/util/tools/Fox.scala index eb0ef55f6cf..842e41fe7bb 100644 --- a/util/src/main/scala/com/scalableminds/util/tools/Fox.scala +++ b/util/src/main/scala/com/scalableminds/util/tools/Fox.scala @@ -33,7 +33,7 @@ trait FoxImplicits { implicit def try2Fox[T](t: Try[T])(implicit ec: ExecutionContext): Fox[T] = t match { case Success(result) => Fox.successful(result) - case scala.util.Failure(e) => Fox.failure(s"${e.toString}") + case scala.util.Failure(e) => Fox.failure(e.toString) } implicit def fox2FutureBox[T](f: Fox[T]): Future[Box[T]] = From 947a4c873edbc5aaa8e59ee299bd33ff40a1cf6e Mon Sep 17 00:00:00 2001 From: leowe <13684843+leowe@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:47:31 +0200 Subject: [PATCH 35/63] Update webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala Co-authored-by: Florian M --- .../datastore/services/MeshFileService.scala | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index 290d0f0947e..a7ffaf86f78 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -143,14 +143,10 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC meshFilePath will return Fox.empty, while meshfiles with one marked as empty, will return Fox.successful(null) */ def mappingNameForMeshFile(meshFilePath: Path, meshFileVersion: Long): Fox[String] = - if (meshFileVersion == 0) { - safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => - cachedMeshFile.reader.string().getAttr("/", "metadata/mapping_name") - } ?~> "mesh.file.readEncoding.failed" - } else { - safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => - cachedMeshFile.reader.string().getAttr("/", "mapping_name") - } ?~> "mesh.file.readEncoding.failed" + val attributeName = if (meshFileVersion == 0) "metadata/mapping_name" else "mapping_name" + safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => + cachedMeshFile.reader.string().getAttr("/", attributeName) + } ?~> "mesh.file.readEncoding.failed" } def mappingVersionForMeshFile(meshFilePath: Path): Long = From c76c090a556323f795bace2aff998f652f6c3c17 Mon Sep 17 00:00:00 2001 From: leowe <13684843+leowe@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:47:42 +0200 Subject: [PATCH 36/63] Update webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala Co-authored-by: Florian M --- .../webknossos/datastore/storage/Hdf5FileCache.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala index 00569aa1c35..f2c61d800b8 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala @@ -64,7 +64,7 @@ object CachedHdf5Utils { _ <- if (filePath.toFile.exists()) { Full(true) } else { - Empty ~> "mesh.file.open.failed" + Failure("mesh.file.open.failed") } result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { block From 5ab2f836aabfae1fa064334821203cf1bcf088b2 Mon Sep 17 00:00:00 2001 From: leowe Date: Fri, 30 Sep 2022 11:31:10 +0200 Subject: [PATCH 37/63] incorporate pr feedback (move methods, renaming) --- .../controllers/MeshController.scala | 8 - .../datastore/services/MeshFileService.scala | 141 +++++++++--------- .../datastore/storage/Hdf5FileCache.scala | 23 ++- 3 files changed, 81 insertions(+), 91 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala index ad05c79ac87..7c3e9e4b920 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/MeshController.scala @@ -8,11 +8,8 @@ import play.api.i18n.Messages import play.api.libs.json.Json import play.api.mvc.{Action, AnyContent, PlayBodyParsers} -import io.swagger.annotations.{Api, ApiOperation} - import scala.concurrent.ExecutionContext.Implicits.global -@Api(tags = Array("datastore")) class MeshController @Inject()( accessTokenService: DataStoreAccessTokenService, meshFileService: MeshFileService, @@ -22,7 +19,6 @@ class MeshController @Inject()( override def allowRemoteOrigin: Boolean = true - @ApiOperation(hidden = true, value = "") def listMeshFiles(token: Option[String], organizationName: String, dataSetName: String, @@ -36,7 +32,6 @@ class MeshController @Inject()( } } - @ApiOperation(hidden = true, value = "") def listMeshChunksForSegmentV0(token: Option[String], organizationName: String, dataSetName: String, @@ -56,7 +51,6 @@ class MeshController @Inject()( } } - @ApiOperation(hidden = true, value = "") def listMeshChunksForSegmentForVersion(token: Option[String], organizationName: String, dataSetName: String, @@ -78,7 +72,6 @@ class MeshController @Inject()( } } - @ApiOperation(hidden = true, value = "") def readMeshChunkV0(token: Option[String], organizationName: String, dataSetName: String, @@ -99,7 +92,6 @@ class MeshController @Inject()( } } - @ApiOperation(hidden = true, value = "") def readMeshChunkForVersion(token: Option[String], organizationName: String, dataSetName: String, diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index a7ffaf86f78..f9c4fea07db 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -5,7 +5,7 @@ import com.scalableminds.util.geometry.{Vec3Float, Vec3Int} import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.DataStoreConfig -import com.scalableminds.webknossos.datastore.storage.CachedHdf5Utils.{safeExecute, safeExecuteBox} +import com.scalableminds.webknossos.datastore.storage.CachedHdf5Utils.executeWithCachedHdf5 import com.scalableminds.webknossos.datastore.storage.{CachedHdf5File, Hdf5FileCache} import com.typesafe.scalalogging.LazyLogging import net.liftweb.common.Box @@ -72,6 +72,66 @@ case class NeuroglancerSegmentInfo(chunkShape: Vec3Float, chunkPositions: List[List[Vec3Int]], chunkByteOffsets: List[List[Int]]) +object NeuroglancerSegmentInfo { + def fromBytes(manifest: Array[Byte]): NeuroglancerSegmentInfo = { + // All Ints here should be UInt32 per spec. We assume that the sign bit is not necessary (the encoded values are at most 2^31). + // But they all are used to index into Arrays and JVM doesn't allow for Long Array Indexes, + // we can't convert them. + val byteInput = new ByteArrayInputStream(manifest) + val dis = new LittleEndianDataInputStream(byteInput) + + val chunkShape = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) + val gridOrigin = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) + + val numLods = dis.readInt + + val lodScales = new Array[Float](numLods) + for (d <- 0 until numLods) { + lodScales(d) = dis.readFloat + } + + val vertexOffsets = new Array[Vec3Float](numLods) + for (d <- 0 until numLods) { + vertexOffsets(d) = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) + } + + val numChunksPerLod = new Array[Int](numLods) + for (lod <- 0 until numLods) { + numChunksPerLod(lod) = dis.readInt() + } + + val chunkPositionsList = new ListBuffer[List[Vec3Int]] + val chunkSizes = new ListBuffer[List[Int]] + for (lod <- 0 until numLods) { + val currentChunkPositions = (ListBuffer[Int](), ListBuffer[Int](), ListBuffer[Int]()) + for (row <- 0 until 3; _ <- 0 until numChunksPerLod(lod)) { + row match { + case 0 => currentChunkPositions._1.append(dis.readInt) + case 1 => currentChunkPositions._2.append(dis.readInt) + case 2 => currentChunkPositions._3.append(dis.readInt) + } + } + + chunkPositionsList.append(currentChunkPositions.zipped.map(Vec3Int(_, _, _)).toList) + + val currentChunkSizes = ListBuffer[Int]() + for (_ <- 0 until numChunksPerLod(lod)) { + currentChunkSizes.append(dis.readInt) + } + chunkSizes.append(currentChunkSizes.toList) + } + + NeuroglancerSegmentInfo(chunkShape, + gridOrigin, + numLods, + lodScales, + vertexOffsets, + numChunksPerLod, + chunkPositionsList.toList, + chunkSizes.toList) + } +} + case class MeshChunk(position: Vec3Float, byteOffset: Int, byteSize: Int) object MeshChunk { @@ -142,15 +202,15 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC Note that null is a valid value here for once. Meshfiles with no information about the meshFilePath will return Fox.empty, while meshfiles with one marked as empty, will return Fox.successful(null) */ - def mappingNameForMeshFile(meshFilePath: Path, meshFileVersion: Long): Fox[String] = + def mappingNameForMeshFile(meshFilePath: Path, meshFileVersion: Long): Fox[String] = { val attributeName = if (meshFileVersion == 0) "metadata/mapping_name" else "mapping_name" - safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => + executeWithCachedHdf5(meshFilePath, meshFileCache) { cachedMeshFile => cachedMeshFile.reader.string().getAttr("/", attributeName) } ?~> "mesh.file.readEncoding.failed" - } + } def mappingVersionForMeshFile(meshFilePath: Path): Long = - safeExecuteBox(meshFilePath, meshFileCache) { cachedMeshFile => + executeWithCachedHdf5(meshFilePath, meshFileCache) { cachedMeshFile => cachedMeshFile.reader.int64().getAttr("/", "version") }.toOption.getOrElse(0) @@ -166,7 +226,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") - safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => + executeWithCachedHdf5(meshFilePath, meshFileCache) { cachedMeshFile => val chunkPositionLiterals = cachedMeshFile.reader .`object`() .getAllGroupMembers(s"/${listMeshChunksRequest.segmentId}/$defaultLevelOfDetail") @@ -188,7 +248,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${listMeshChunksRequest.meshFile}.$meshFileExtension") - safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => + executeWithCachedHdf5(meshFilePath, meshFileCache) { cachedMeshFile => val segmentId = listMeshChunksRequest.segmentId val encoding = cachedMeshFile.reader.string().getAttr("/", "mesh_format") val lodScaleMultiplier = cachedMeshFile.reader.float64().getAttr("/", "lod_scale_multiplier") @@ -199,9 +259,9 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC val manifest = cachedMeshFile.reader .uint8() .readArrayBlockWithOffset("/neuroglancer", (neuroglancerEnd - neuroglancerStart).toInt, neuroglancerStart) - val segmentInfo = parseNeuroglancerManifest(manifest) - val meshfile = getChunksFromSegmentInfo(segmentInfo, lodScaleMultiplier, neuroglancerStart) - WebknossosSegmentInfo(transform = transform, meshFormat = encoding, chunks = meshfile) + val segmentInfo = NeuroglancerSegmentInfo.fromBytes(manifest) + val processedSegmentInfo = getChunksFromSegmentInfo(segmentInfo, lodScaleMultiplier, neuroglancerStart) + WebknossosSegmentInfo(transform = transform, meshFormat = encoding, chunks = processedSegmentInfo) } } @@ -241,63 +301,6 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .toList MeshSegmentInfo(chunkShape = segmentInfo.chunkShape, gridOrigin = segmentInfo.gridOrigin, lods = meshfileLods) } - private def parseNeuroglancerManifest(manifest: Array[Byte]): NeuroglancerSegmentInfo = { - // All Ints here should be UInt32 per spec. We assume that the sign bit is not necessary (the encoded values are at most 2^31). - // But they all are used to index into Arrays and JVM doesn't allow for Long Array Indexes, - // we can't convert them. - val byteInput = new ByteArrayInputStream(manifest) - val dis = new LittleEndianDataInputStream(byteInput) - - val chunkShape = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) - val gridOrigin = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) - - val numLods = dis.readInt - - val lodScales = new Array[Float](numLods) - for (d <- 0 until numLods) { - lodScales(d) = dis.readFloat - } - - val vertexOffsets = new Array[Vec3Float](numLods) - for (d <- 0 until numLods) { - vertexOffsets(d) = Vec3Float(x = dis.readFloat, y = dis.readFloat, z = dis.readFloat) - } - - val numChunksPerLod = new Array[Int](numLods) - for (lod <- 0 until numLods) { - numChunksPerLod(lod) = dis.readInt() - } - - val chunkPositionsList = new ListBuffer[List[Vec3Int]] - val chunkSizes = new ListBuffer[List[Int]] - for (lod <- 0 until numLods) { - val currentChunkPositions = (ListBuffer[Int](), ListBuffer[Int](), ListBuffer[Int]()) - for (row <- 0 until 3; _ <- 0 until numChunksPerLod(lod)) { - row match { - case 0 => currentChunkPositions._1.append(dis.readInt) - case 1 => currentChunkPositions._2.append(dis.readInt) - case 2 => currentChunkPositions._3.append(dis.readInt) - } - } - - chunkPositionsList.append(currentChunkPositions.zipped.map(Vec3Int(_, _, _)).toList) - - val currentChunkSizes = ListBuffer[Int]() - for (_ <- 0 until numChunksPerLod(lod)) { - currentChunkSizes.append(dis.readInt) - } - chunkSizes.append(currentChunkSizes.toList) - } - - NeuroglancerSegmentInfo(chunkShape, - gridOrigin, - numLods, - lodScales, - vertexOffsets, - numChunksPerLod, - chunkPositionsList.toList, - chunkSizes.toList) - } private def getNeuroglancerOffsets(segmentId: Long, cachedMeshFile: CachedHdf5File): (Long, Long) = { val nBuckets = cachedMeshFile.reader.uint64().getAttr("/", "n_buckets") @@ -330,7 +333,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${meshChunkDataRequest.meshFile}.$meshFileExtension") - safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => + executeWithCachedHdf5(meshFilePath, meshFileCache) { cachedMeshFile => val encoding = cachedMeshFile.reader.string().getAttr("/", "metadata/encoding") val key = s"/${meshChunkDataRequest.segmentId}/$defaultLevelOfDetail/${positionLiteral(meshChunkDataRequest.position)}" @@ -351,7 +354,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC .resolve(meshesDir) .resolve(s"${meshChunkDataRequest.meshFile}.$meshFileExtension") - safeExecute(meshFilePath, meshFileCache) { cachedMeshFile => + executeWithCachedHdf5(meshFilePath, meshFileCache) { cachedMeshFile => val meshFormat = cachedMeshFile.reader.string().getAttr("/", "mesh_format") val data = cachedMeshFile.reader diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala index f2c61d800b8..1bb024dd7f0 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala @@ -5,7 +5,7 @@ import com.scalableminds.util.cache.LRUConcurrentCache import com.scalableminds.util.tools.Fox import com.scalableminds.util.tools.Fox.{bool2Fox, try2Fox} import com.scalableminds.webknossos.datastore.dataformats.SafeCachable -import net.liftweb.common.{Box, Full, Empty} +import net.liftweb.common.{Box, Failure, Full} import java.nio.file.Path import scala.concurrent.ExecutionContext @@ -50,24 +50,19 @@ class Hdf5FileCache(val maxEntries: Int) extends LRUConcurrentCache[String, Cach } object CachedHdf5Utils { - def safeExecute[T](filePath: Path, meshFileCache: Hdf5FileCache)(block: CachedHdf5File => T)( - implicit ec: ExecutionContext): Fox[T] = - for { - _ <- bool2Fox(filePath.toFile.exists()) ?~> "mesh.file.open.failed" - result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { - block - }.toFox - } yield result - - def safeExecuteBox[T](filePath: Path, meshFileCache: Hdf5FileCache)(block: CachedHdf5File => T): Box[T] = + def executeWithCachedHdf5[T](filePath: Path, meshFileCache: Hdf5FileCache)(block: CachedHdf5File => T): Box[T] = for { _ <- if (filePath.toFile.exists()) { Full(true) } else { Failure("mesh.file.open.failed") } - result <- Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { + result = Using(meshFileCache.withCache(filePath)(CachedHdf5File.fromPath)) { block - }.toOption - } yield result + } + boxedResult <- result match { + case scala.util.Success(result) => Full(result) + case scala.util.Failure(e) => Failure(e.toString) + } + } yield boxedResult } From 57ad538b10d38dddb6f097000e559076ee74dc97 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 30 Sep 2022 13:17:02 +0200 Subject: [PATCH 38/63] make all meshes smooth --- .../oxalis/controller/scene_controller.ts | 10 ++++------ frontend/javascripts/oxalis/default_state.ts | 1 + .../oxalis/model/sagas/isosurface_saga.ts | 15 ++++++++++++++- frontend/javascripts/oxalis/store.ts | 1 + .../segments_tab/segments_view.tsx | 17 ++++++++++++++++- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index d7074f48d18..dc61fb827da 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -39,6 +39,7 @@ import constants, { import window from "libs/window"; import { setSceneController } from "oxalis/controller/scene_controller_provider"; import { getSegmentColorAsHSL } from "oxalis/model/accessors/volumetracing_accessor"; +import { mergeVertices } from "three/examples/jsm/utils/BufferGeometryUtils.js"; const CUBE_COLOR = 0x999999; @@ -200,9 +201,8 @@ class SceneController { constructIsosurfaceMesh(cellId: number, geometry: THREE.BufferGeometry, passive: boolean) { const color = this.getColorObjectForSegment(cellId); - const meshMaterial = new THREE.MeshStandardMaterial({ + const meshMaterial = new THREE.MeshLambertMaterial({ color, - // flatShading: true, }); meshMaterial.side = THREE.FrontSide; meshMaterial.transparent = true; @@ -259,10 +259,8 @@ class SceneController { let bufferGeometry = new THREE.BufferGeometry(); bufferGeometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3)); - // @ts-ignore - if (window.__isosurfaceMergeVertices) { - // @ts-ignore mergeVertices - bufferGeometry = THREE.BufferGeometryUtils.mergeVertices(bufferGeometry); + if (Store.getState().userConfiguration.useSmoothMeshes) { + bufferGeometry = mergeVertices(bufferGeometry); } bufferGeometry.computeVertexNormals(); diff --git a/frontend/javascripts/oxalis/default_state.ts b/frontend/javascripts/oxalis/default_state.ts index fd5f91ceb4d..c0480b58feb 100644 --- a/frontend/javascripts/oxalis/default_state.ts +++ b/frontend/javascripts/oxalis/default_state.ts @@ -78,6 +78,7 @@ const defaultState: OxalisState = { fillMode: FillModeEnum._2D, interpolationMode: InterpolationModeEnum.INTERPOLATE, useLegacyBindings: false, + useSmoothMeshes: true, }, temporaryConfiguration: { viewMode: Constants.MODE_PLANE_TRACING, diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 62c49df9b4a..67d8b1d40f7 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -4,6 +4,7 @@ import { V3 } from "libs/mjs"; import { sleep } from "libs/utils"; import ErrorHandling from "libs/error_handling"; import type { APIDataLayer } from "types/api_flow_types"; +import { mergeVertices } from "three/examples/jsm/utils/BufferGeometryUtils.js"; import { ResolutionInfo, @@ -589,6 +590,7 @@ function* loadPrecomputedMeshForSegmentId( const sceneController = yield* call(getSceneController); if ("position" in chunk) { + // V3 const dracoData = yield* call( meshV3.getMeshfileChunkData, dataset.dataStore.url, @@ -601,6 +603,7 @@ function* loadPrecomputedMeshForSegmentId( const loader = getDracoLoader(); const geometry = yield* call(loader.decodeDracoFileAsync, dracoData); + // Compute vertex normals to achieve smooth shading geometry.computeVertexNormals(); yield* call( @@ -612,6 +615,7 @@ function* loadPrecomputedMeshForSegmentId( true, ); } else { + // V0 const stlData = yield* call( meshV0.getMeshfileChunkData, dataset.dataStore.url, @@ -621,7 +625,16 @@ function* loadPrecomputedMeshForSegmentId( id, chunk, ); - const geometry = yield* call(parseStlBuffer, stlData); + let geometry = yield* call(parseStlBuffer, stlData); + + // Delete existing vertex normals (since these are not interpolated + // across faces). + geometry.deleteAttribute("normal"); + // Ensure that vertices of adjacent faces are shared. + geometry = mergeVertices(geometry); + // Recompute normals to achieve smooth shading + geometry.computeVertexNormals(); + yield* call( { context: sceneController, fn: sceneController.addIsosurfaceFromGeometry }, geometry, diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index 56519199dc6..fde163e83cf 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -325,6 +325,7 @@ export type UserConfiguration = { readonly fillMode: FillMode; readonly interpolationMode: InterpolationMode; readonly useLegacyBindings: boolean; + readonly useSmoothMeshes: boolean; }; export type RecommendedConfiguration = Partial< UserConfiguration & diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index 40412e120f8..ccf1b37527f 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -31,7 +31,10 @@ import { } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { setPositionAction } from "oxalis/model/actions/flycam_actions"; import { startComputeMeshFileJob, getJobs } from "admin/admin_rest_api"; -import { updateTemporarySettingAction } from "oxalis/model/actions/settings_actions"; +import { + updateTemporarySettingAction, + updateUserSettingAction, +} from "oxalis/model/actions/settings_actions"; import { updateSegmentAction, setActiveCellAction, @@ -51,6 +54,7 @@ import type { import Store from "oxalis/store"; import Toast from "libs/toast"; import features from "features"; +import { SwitchSetting } from "oxalis/view/components/setting_input_views"; const { Option } = Select; // Interval in ms to check for running mesh file computation jobs for this dataset @@ -83,6 +87,7 @@ type StateProps = { preferredQualityForMeshPrecomputation: number; preferredQualityForMeshAdHocComputation: number; resolutionInfoOfVisibleSegmentationLayer: ResolutionInfo; + useSmoothMeshes: boolean; }; const mapStateToProps = (state: OxalisState): StateProps => { @@ -124,6 +129,7 @@ const mapStateToProps = (state: OxalisState): StateProps => { preferredQualityForMeshAdHocComputation: state.temporaryConfiguration.preferredQualityForMeshAdHocComputation, resolutionInfoOfVisibleSegmentationLayer: getResolutionInfoOfVisibleSegmentationLayer(state), + useSmoothMeshes: state.userConfiguration.useSmoothMeshes, }; }; @@ -425,9 +431,18 @@ class SegmentsView extends React.Component { const { preferredQualityForMeshAdHocComputation, resolutionInfoOfVisibleSegmentationLayer: datasetResolutionInfo, + useSmoothMeshes, } = this.props; return (
+ { + Store.dispatch(updateUserSettingAction("useSmoothMeshes", value)); + }} + labelSpan={18} + />
Quality for Ad-Hoc Mesh Computation:
From 2710f81b4092f86df01cc9e66d450983e656f5ad Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 30 Sep 2022 13:17:30 +0200 Subject: [PATCH 39/63] generalize switch setting to allow for different width of label --- .../oxalis/view/components/setting_input_views.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/view/components/setting_input_views.tsx b/frontend/javascripts/oxalis/view/components/setting_input_views.tsx index da964db2a78..7901b0f7133 100644 --- a/frontend/javascripts/oxalis/view/components/setting_input_views.tsx +++ b/frontend/javascripts/oxalis/view/components/setting_input_views.tsx @@ -9,6 +9,8 @@ import messages from "messages"; const ROW_GUTTER = 1; +const ANTD_TOTAL_SPAN = 24; + // Always the left part: export const SETTING_LEFT_SPAN = 10; @@ -187,6 +189,7 @@ type SwitchSettingProps = { disabled: boolean; tooltipText: string | null | undefined; loading: boolean; + labelSpan?: number | null; }; export class SwitchSetting extends React.PureComponent { static defaultProps = { @@ -196,13 +199,15 @@ export class SwitchSetting extends React.PureComponent { }; render() { - const { label, onChange, value, disabled, tooltipText, loading } = this.props; + const { label, onChange, value, disabled, tooltipText, loading, labelSpan } = this.props; + const leftSpanValue = labelSpan || SETTING_LEFT_SPAN; + const rightSpanValue = labelSpan != null ? ANTD_TOTAL_SPAN - leftSpanValue : SETTING_RIGHT_SPAN; return ( - + - + {/* This div is necessary for the tooltip to be displayed */}
Date: Fri, 30 Sep 2022 13:19:05 +0200 Subject: [PATCH 40/63] remove setting for smooth meshes again (always use smooth meshes) --- .../javascripts/oxalis/controller/scene_controller.ts | 6 ++---- frontend/javascripts/oxalis/default_state.ts | 1 - frontend/javascripts/oxalis/store.ts | 1 - .../right-border-tabs/segments_tab/segments_view.tsx | 11 ----------- 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index dc61fb827da..3ea94d45345 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -259,11 +259,9 @@ class SceneController { let bufferGeometry = new THREE.BufferGeometry(); bufferGeometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3)); - if (Store.getState().userConfiguration.useSmoothMeshes) { - bufferGeometry = mergeVertices(bufferGeometry); - } - + bufferGeometry = mergeVertices(bufferGeometry); bufferGeometry.computeVertexNormals(); + this.addIsosurfaceFromGeometry(bufferGeometry, segmentationId, passive); } diff --git a/frontend/javascripts/oxalis/default_state.ts b/frontend/javascripts/oxalis/default_state.ts index c0480b58feb..fd5f91ceb4d 100644 --- a/frontend/javascripts/oxalis/default_state.ts +++ b/frontend/javascripts/oxalis/default_state.ts @@ -78,7 +78,6 @@ const defaultState: OxalisState = { fillMode: FillModeEnum._2D, interpolationMode: InterpolationModeEnum.INTERPOLATE, useLegacyBindings: false, - useSmoothMeshes: true, }, temporaryConfiguration: { viewMode: Constants.MODE_PLANE_TRACING, diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index fde163e83cf..56519199dc6 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -325,7 +325,6 @@ export type UserConfiguration = { readonly fillMode: FillMode; readonly interpolationMode: InterpolationMode; readonly useLegacyBindings: boolean; - readonly useSmoothMeshes: boolean; }; export type RecommendedConfiguration = Partial< UserConfiguration & diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index ccf1b37527f..297152f1bc7 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -87,7 +87,6 @@ type StateProps = { preferredQualityForMeshPrecomputation: number; preferredQualityForMeshAdHocComputation: number; resolutionInfoOfVisibleSegmentationLayer: ResolutionInfo; - useSmoothMeshes: boolean; }; const mapStateToProps = (state: OxalisState): StateProps => { @@ -129,7 +128,6 @@ const mapStateToProps = (state: OxalisState): StateProps => { preferredQualityForMeshAdHocComputation: state.temporaryConfiguration.preferredQualityForMeshAdHocComputation, resolutionInfoOfVisibleSegmentationLayer: getResolutionInfoOfVisibleSegmentationLayer(state), - useSmoothMeshes: state.userConfiguration.useSmoothMeshes, }; }; @@ -431,18 +429,9 @@ class SegmentsView extends React.Component { const { preferredQualityForMeshAdHocComputation, resolutionInfoOfVisibleSegmentationLayer: datasetResolutionInfo, - useSmoothMeshes, } = this.props; return (
- { - Store.dispatch(updateUserSettingAction("useSmoothMeshes", value)); - }} - labelSpan={18} - />
Quality for Ad-Hoc Mesh Computation:
From c6ddc483279aaeca1c39e3fb396dafd7652627fa Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 30 Sep 2022 13:19:23 +0200 Subject: [PATCH 41/63] improve lighting and expose window.testLights method for tuning parameters in dev --- .../oxalis/controller/scene_controller.ts | 64 +++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 3ea94d45345..386e435e701 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -309,12 +309,66 @@ class SceneController { // At the moment, we only attach an AmbientLight for the isosurfaces group. // The PlaneView attaches a directional light directly to the TD camera, // so that the light moves along the cam. - const ambientLightForIsosurfaces = new THREE.AmbientLight(0x404040, 15); // soft white light - - this.isosurfacesRootGroup.add(ambientLightForIsosurfaces); - const ambientLightForMeshes = new THREE.AmbientLight(0x404040, 15); // soft white light + // const ambientLightForIsosurfaces = new THREE.AmbientLight(0x404040, 15); // soft white light + // this.isosurfacesRootGroup.add(ambientLightForIsosurfaces); + + // const ambientLightForMeshes = new THREE.AmbientLight(0x404040, 5); // soft white light + // this.meshesRootGroup.add(ambientLightForMeshes); + + let unsubscribe = () => {}; + const testLights = (a: number, b: number, c: number, d: number) => { + unsubscribe(); + const ambientLight = new THREE.AmbientLight(2105376, a); + + const directionalLight = new THREE.DirectionalLight(16777215, b); + directionalLight.position.x = 1; + directionalLight.position.y = 1; + directionalLight.position.z = 1; + directionalLight.position.normalize(); + + // const max = 10000000000; + + const directionalLight2 = new THREE.DirectionalLight(16777215, c); + directionalLight2.position.x = -1; + directionalLight2.position.y = -1; + directionalLight2.position.z = -1; + directionalLight2.position.normalize(); + + const pointLight = new THREE.PointLight(16777215, d); + pointLight.position.x = 0; + pointLight.position.y = -25; + pointLight.position.z = 10; + + this.isosurfacesRootGroup.add(ambientLight); + this.isosurfacesRootGroup.add(directionalLight); + this.isosurfacesRootGroup.add(directionalLight2); + this.isosurfacesRootGroup.add(pointLight); + + unsubscribe = () => { + this.isosurfacesRootGroup.remove(ambientLight); + this.isosurfacesRootGroup.remove(directionalLight); + this.isosurfacesRootGroup.remove(directionalLight2); + this.isosurfacesRootGroup.remove(pointLight); + }; + }; - this.meshesRootGroup.add(ambientLightForMeshes); + testLights(30, 5, 5, 5); + // @ts-ignore + window.testLights = testLights; + // directionalLight.position.x = 2 * max; + // directionalLight.position.y = 0; + // directionalLight.position.z = 2 * max; + // pointLight.position.x = max / 2; + // pointLight.position.y = max / 2; + // pointLight.position.z = 2 * max; + + // const light1 = new THREE.DirectionalLight(0xefefff, 12); + // light1.position.set(1, 1, 1).normalize(); + // this.isosurfacesRootGroup.add(light1); + + // const light2 = new THREE.DirectionalLight(0xffefef, 8); + // light2.position.set(-1, -1, -1).normalize(); + // this.isosurfacesRootGroup.add(light2); } removeSTL(id: string): void { From 5c2d8663297827d4914a4aa4c229c04ef7338db4 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 30 Sep 2022 13:19:58 +0200 Subject: [PATCH 42/63] remove unused imports --- .../view/right-border-tabs/segments_tab/segments_view.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index 297152f1bc7..40412e120f8 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -31,10 +31,7 @@ import { } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { setPositionAction } from "oxalis/model/actions/flycam_actions"; import { startComputeMeshFileJob, getJobs } from "admin/admin_rest_api"; -import { - updateTemporarySettingAction, - updateUserSettingAction, -} from "oxalis/model/actions/settings_actions"; +import { updateTemporarySettingAction } from "oxalis/model/actions/settings_actions"; import { updateSegmentAction, setActiveCellAction, @@ -54,7 +51,6 @@ import type { import Store from "oxalis/store"; import Toast from "libs/toast"; import features from "features"; -import { SwitchSetting } from "oxalis/view/components/setting_input_views"; const { Option } = Select; // Interval in ms to check for running mesh file computation jobs for this dataset From 9f5a4eb31e83ec2fac729f8b48a4ee39469e14aa Mon Sep 17 00:00:00 2001 From: leowe Date: Fri, 30 Sep 2022 13:42:34 +0200 Subject: [PATCH 43/63] remove unused imports --- .../webknossos/datastore/storage/Hdf5FileCache.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala index 1bb024dd7f0..799f6c5785d 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/Hdf5FileCache.scala @@ -2,13 +2,10 @@ package com.scalableminds.webknossos.datastore.storage import ch.systemsx.cisd.hdf5.{HDF5FactoryProvider, IHDF5Reader} import com.scalableminds.util.cache.LRUConcurrentCache -import com.scalableminds.util.tools.Fox -import com.scalableminds.util.tools.Fox.{bool2Fox, try2Fox} import com.scalableminds.webknossos.datastore.dataformats.SafeCachable import net.liftweb.common.{Box, Failure, Full} import java.nio.file.Path -import scala.concurrent.ExecutionContext import scala.util.Using case class CachedHdf5File(reader: IHDF5Reader) extends SafeCachable with AutoCloseable { From d5fc95a729ffdbc65a60eb1dc46ec5d36d08fb03 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 30 Sep 2022 13:52:37 +0200 Subject: [PATCH 44/63] use formatVersion of mesh file to decide which routes to use --- frontend/javascripts/admin/admin_rest_api.ts | 12 ++++++++++-- frontend/javascripts/oxalis/api/api_latest.ts | 3 +-- .../oxalis/model/sagas/isosurface_saga.ts | 16 +++++++++++++--- .../segments_tab/segments_view_helper.tsx | 8 +------- frontend/javascripts/types/api_flow_types.ts | 4 ++++ 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts index 6e560f3b50e..c8065cf160e 100644 --- a/frontend/javascripts/admin/admin_rest_api.ts +++ b/frontend/javascripts/admin/admin_rest_api.ts @@ -2071,16 +2071,24 @@ export function getEditableAgglomerateSkeleton( ); } -export function getMeshfilesForDatasetLayer( +export async function getMeshfilesForDatasetLayer( dataStoreUrl: string, datasetId: APIDatasetId, layerName: string, ): Promise> { - return doWithToken((token) => + const meshFiles: Array = await doWithToken((token) => Request.receiveJSON( `${dataStoreUrl}/data/datasets/${datasetId.owningOrganization}/${datasetId.name}/layers/${layerName}/meshes?token=${token}`, ), ); + + for (const file of meshFiles) { + if (file.mappingName === "") { + file.mappingName = undefined; + } + } + + return meshFiles; } // ### Connectomes diff --git a/frontend/javascripts/oxalis/api/api_latest.ts b/frontend/javascripts/oxalis/api/api_latest.ts index 38be03d21fa..c2762f3df2c 100644 --- a/frontend/javascripts/oxalis/api/api_latest.ts +++ b/frontend/javascripts/oxalis/api/api_latest.ts @@ -1841,8 +1841,7 @@ class DataApi { const { mappingName, meshFileName } = currentMeshFile; - // todo: remove second condition - if (mappingName != null && mappingName !== "") { + if (mappingName != null) { const activeMapping = this.getActiveMapping(effectiveLayerName); if (mappingName !== activeMapping) { diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 67d8b1d40f7..a07078d7610 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -548,10 +548,20 @@ function* loadPrecomputedMeshForSegmentId( let availableChunks = null; - // todo: dont hardcode - const version = meshFileName.includes("4-4") ? 0 : 3; + const meshFile = yield* select((state) => + (state.localSegmentationData[layerName].availableMeshFiles || []).find( + (file) => file.meshFileName === meshFileName, + ), + ); + if (!meshFile) { + throw new Error("Could not find requested mesh file."); + } + + const version = meshFile.formatVersion; try { - if (version === 3) { + // todo: should actually check against 3, but some new mesh files + // still have version 2 ? + if (version >= 2) { const segmentInfo = yield* call( meshV3.getMeshfileChunksForSegment, dataset.dataStore.url, diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx index 2420e928aab..721a109ae4c 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx @@ -84,13 +84,7 @@ export function withMappingActivationConfirmation> // If the mapping name is undefined, no mapping is specified. In that case never show the activation modal. // In contrast, if the mapping name is null, this indicates that all mappings should be specifically disabled. - // todo: remove last condition - if ( - mappingName === undefined || - layerName == null || - mappingName === enabledMappingName || - mappingName === "" - ) { + if (mappingName === undefined || layerName == null || mappingName === enabledMappingName) { // @ts-expect-error ts-migrate(2322) FIXME: Type 'Omit, ... Remove this comment to see the full error message return ; } diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts index 863936f9f0d..8754552544d 100644 --- a/frontend/javascripts/types/api_flow_types.ts +++ b/frontend/javascripts/types/api_flow_types.ts @@ -693,6 +693,10 @@ export type ServerEditableMapping = { export type APIMeshFile = { meshFileName: string; mappingName?: string | null | undefined; + // 0 - is the first mesh file version + // 1-2 - skipped for consistency with VX artifact versioning + // 3 - (some artifacts might have used 2, too) is the newer version with draco encoding. + formatVersion: number; }; export type APIConnectomeFile = { connectomeFileName: string; From 4dab4a43d361a02604e4056492d4ba0c8248f33a Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 30 Sep 2022 16:02:46 +0200 Subject: [PATCH 45/63] fix failing tests by simply copying DracoLoader and BufferGeometryUtils from threejs package to repo, because of ERR_REQUIRE_ESM error --- frontend/javascripts/admin/api/token.ts | 1 + .../javascripts/libs/BufferGeometryUtils.ts | 839 ++++++++++++++++++ frontend/javascripts/libs/DRACOLoader.ts | 470 ++++++++++ frontend/javascripts/libs/draco.ts | 3 +- .../oxalis/controller/scene_controller.ts | 2 +- .../oxalis/model/sagas/isosurface_saga.ts | 2 +- 6 files changed, 1314 insertions(+), 3 deletions(-) create mode 100644 frontend/javascripts/libs/BufferGeometryUtils.ts create mode 100644 frontend/javascripts/libs/DRACOLoader.ts diff --git a/frontend/javascripts/admin/api/token.ts b/frontend/javascripts/admin/api/token.ts index 4424ca662c6..9b0de025484 100644 --- a/frontend/javascripts/admin/api/token.ts +++ b/frontend/javascripts/admin/api/token.ts @@ -1,3 +1,4 @@ +import { location } from "libs/window"; import Request from "libs/request"; import * as Utils from "libs/utils"; diff --git a/frontend/javascripts/libs/BufferGeometryUtils.ts b/frontend/javascripts/libs/BufferGeometryUtils.ts new file mode 100644 index 00000000000..7b51c73a324 --- /dev/null +++ b/frontend/javascripts/libs/BufferGeometryUtils.ts @@ -0,0 +1,839 @@ +// @ts-nocheck +// Copied from three/examples/js/utils/BufferGeometryUtils.js to fix ERR_REQUIRE_ESM error. +import { + BufferAttribute, + BufferGeometry, + Float32BufferAttribute, + InterleavedBuffer, + InterleavedBufferAttribute, + TriangleFanDrawMode, + TriangleStripDrawMode, + TrianglesDrawMode, + Vector3, +} from "three"; + +function computeTangents(geometry) { + geometry.computeTangents(); + console.warn( + "THREE.BufferGeometryUtils: .computeTangents() has been removed. Use BufferGeometry.computeTangents() instead.", + ); +} + +/** + * @param {Array} geometries + * @param {Boolean} useGroups + * @return {BufferGeometry} + */ +function mergeBufferGeometries(geometries, useGroups = false) { + const isIndexed = geometries[0].index !== null; + + const attributesUsed = new Set(Object.keys(geometries[0].attributes)); + const morphAttributesUsed = new Set(Object.keys(geometries[0].morphAttributes)); + + const attributes = {}; + const morphAttributes = {}; + + const morphTargetsRelative = geometries[0].morphTargetsRelative; + + const mergedGeometry = new BufferGeometry(); + + let offset = 0; + + for (let i = 0; i < geometries.length; ++i) { + const geometry = geometries[i]; + let attributesCount = 0; + + // ensure that all geometries are indexed, or none + + if (isIndexed !== (geometry.index !== null)) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index " + + i + + ". All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.", + ); + return null; + } + + // gather attributes, exit early if they're different + + for (const name in geometry.attributes) { + if (!attributesUsed.has(name)) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index " + + i + + '. All geometries must have compatible attributes; make sure "' + + name + + '" attribute exists among all geometries, or in none of them.', + ); + return null; + } + + if (attributes[name] === undefined) attributes[name] = []; + + attributes[name].push(geometry.attributes[name]); + + attributesCount++; + } + + // ensure geometries have the same number of attributes + + if (attributesCount !== attributesUsed.size) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index " + + i + + ". Make sure all geometries have the same number of attributes.", + ); + return null; + } + + // gather morph attributes, exit early if they're different + + if (morphTargetsRelative !== geometry.morphTargetsRelative) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index " + + i + + ". .morphTargetsRelative must be consistent throughout all geometries.", + ); + return null; + } + + for (const name in geometry.morphAttributes) { + if (!morphAttributesUsed.has(name)) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index " + + i + + ". .morphAttributes must be consistent throughout all geometries.", + ); + return null; + } + + if (morphAttributes[name] === undefined) morphAttributes[name] = []; + + morphAttributes[name].push(geometry.morphAttributes[name]); + } + + // gather .userData + + mergedGeometry.userData.mergedUserData = mergedGeometry.userData.mergedUserData || []; + mergedGeometry.userData.mergedUserData.push(geometry.userData); + + if (useGroups) { + let count; + + if (isIndexed) { + count = geometry.index.count; + } else if (geometry.attributes.position !== undefined) { + count = geometry.attributes.position.count; + } else { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index " + + i + + ". The geometry must have either an index or a position attribute", + ); + return null; + } + + mergedGeometry.addGroup(offset, count, i); + + offset += count; + } + } + + // merge indices + + if (isIndexed) { + let indexOffset = 0; + const mergedIndex = []; + + for (let i = 0; i < geometries.length; ++i) { + const index = geometries[i].index; + + for (let j = 0; j < index.count; ++j) { + mergedIndex.push(index.getX(j) + indexOffset); + } + + indexOffset += geometries[i].attributes.position.count; + } + + mergedGeometry.setIndex(mergedIndex); + } + + // merge attributes + + for (const name in attributes) { + const mergedAttribute = mergeBufferAttributes(attributes[name]); + + if (!mergedAttribute) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the " + + name + + " attribute.", + ); + return null; + } + + mergedGeometry.setAttribute(name, mergedAttribute); + } + + // merge morph attributes + + for (const name in morphAttributes) { + const numMorphTargets = morphAttributes[name][0].length; + + if (numMorphTargets === 0) break; + + mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {}; + mergedGeometry.morphAttributes[name] = []; + + for (let i = 0; i < numMorphTargets; ++i) { + const morphAttributesToMerge = []; + + for (let j = 0; j < morphAttributes[name].length; ++j) { + morphAttributesToMerge.push(morphAttributes[name][j][i]); + } + + const mergedMorphAttribute = mergeBufferAttributes(morphAttributesToMerge); + + if (!mergedMorphAttribute) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the " + + name + + " morphAttribute.", + ); + return null; + } + + mergedGeometry.morphAttributes[name].push(mergedMorphAttribute); + } + } + + return mergedGeometry; +} + +/** + * @param {Array} attributes + * @return {BufferAttribute} + */ +function mergeBufferAttributes(attributes) { + let TypedArray; + let itemSize; + let normalized; + let arrayLength = 0; + + for (let i = 0; i < attributes.length; ++i) { + const attribute = attributes[i]; + + if (attribute.isInterleavedBufferAttribute) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. InterleavedBufferAttributes are not supported.", + ); + return null; + } + + if (TypedArray === undefined) TypedArray = attribute.array.constructor; + if (TypedArray !== attribute.array.constructor) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.", + ); + return null; + } + + if (itemSize === undefined) itemSize = attribute.itemSize; + if (itemSize !== attribute.itemSize) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.", + ); + return null; + } + + if (normalized === undefined) normalized = attribute.normalized; + if (normalized !== attribute.normalized) { + console.error( + "THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.", + ); + return null; + } + + arrayLength += attribute.array.length; + } + + const array = new TypedArray(arrayLength); + let offset = 0; + + for (let i = 0; i < attributes.length; ++i) { + array.set(attributes[i].array, offset); + + offset += attributes[i].array.length; + } + + return new BufferAttribute(array, itemSize, normalized); +} + +/** + * @param {Array} attributes + * @return {Array} + */ +function interleaveAttributes(attributes) { + // Interleaves the provided attributes into an InterleavedBuffer and returns + // a set of InterleavedBufferAttributes for each attribute + let TypedArray; + let arrayLength = 0; + let stride = 0; + + // calculate the the length and type of the interleavedBuffer + for (let i = 0, l = attributes.length; i < l; ++i) { + const attribute = attributes[i]; + + if (TypedArray === undefined) TypedArray = attribute.array.constructor; + if (TypedArray !== attribute.array.constructor) { + console.error("AttributeBuffers of different types cannot be interleaved"); + return null; + } + + arrayLength += attribute.array.length; + stride += attribute.itemSize; + } + + // Create the set of buffer attributes + const interleavedBuffer = new InterleavedBuffer(new TypedArray(arrayLength), stride); + let offset = 0; + const res = []; + const getters = ["getX", "getY", "getZ", "getW"]; + const setters = ["setX", "setY", "setZ", "setW"]; + + for (let j = 0, l = attributes.length; j < l; j++) { + const attribute = attributes[j]; + const itemSize = attribute.itemSize; + const count = attribute.count; + const iba = new InterleavedBufferAttribute( + interleavedBuffer, + itemSize, + offset, + attribute.normalized, + ); + res.push(iba); + + offset += itemSize; + + // Move the data for each attribute into the new interleavedBuffer + // at the appropriate offset + for (let c = 0; c < count; c++) { + for (let k = 0; k < itemSize; k++) { + iba[setters[k]](c, attribute[getters[k]](c)); + } + } + } + + return res; +} + +/** + * @param {Array} geometry + * @return {number} + */ +function estimateBytesUsed(geometry) { + // Return the estimated memory used by this geometry in bytes + // Calculate using itemSize, count, and BYTES_PER_ELEMENT to account + // for InterleavedBufferAttributes. + let mem = 0; + for (const name in geometry.attributes) { + const attr = geometry.getAttribute(name); + mem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT; + } + + const indices = geometry.getIndex(); + mem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0; + return mem; +} + +/** + * @param {BufferGeometry} geometry + * @param {number} tolerance + * @return {BufferGeometry>} + */ +function mergeVertices(geometry, tolerance = 1e-4) { + tolerance = Math.max(tolerance, Number.EPSILON); + + // Generate an index buffer if the geometry doesn't have one, or optimize it + // if it's already available. + const hashToIndex = {}; + const indices = geometry.getIndex(); + const positions = geometry.getAttribute("position"); + const vertexCount = indices ? indices.count : positions.count; + + // next value for triangle indices + let nextIndex = 0; + + // attributes and new attribute arrays + const attributeNames = Object.keys(geometry.attributes); + const attrArrays = {}; + const morphAttrsArrays = {}; + const newIndices = []; + const getters = ["getX", "getY", "getZ", "getW"]; + + // initialize the arrays + for (let i = 0, l = attributeNames.length; i < l; i++) { + const name = attributeNames[i]; + + attrArrays[name] = []; + + const morphAttr = geometry.morphAttributes[name]; + if (morphAttr) { + morphAttrsArrays[name] = new Array(morphAttr.length).fill().map(() => []); + } + } + + // convert the error tolerance to an amount of decimal places to truncate to + const decimalShift = Math.log10(1 / tolerance); + const shiftMultiplier = Math.pow(10, decimalShift); + for (let i = 0; i < vertexCount; i++) { + const index = indices ? indices.getX(i) : i; + + // Generate a hash for the vertex attributes at the current index 'i' + let hash = ""; + for (let j = 0, l = attributeNames.length; j < l; j++) { + const name = attributeNames[j]; + const attribute = geometry.getAttribute(name); + const itemSize = attribute.itemSize; + + for (let k = 0; k < itemSize; k++) { + // double tilde truncates the decimal value + hash += `${~~(attribute[getters[k]](index) * shiftMultiplier)},`; + } + } + + // Add another reference to the vertex if it's already + // used by another index + if (hash in hashToIndex) { + newIndices.push(hashToIndex[hash]); + } else { + // copy data to the new index in the attribute arrays + for (let j = 0, l = attributeNames.length; j < l; j++) { + const name = attributeNames[j]; + const attribute = geometry.getAttribute(name); + const morphAttr = geometry.morphAttributes[name]; + const itemSize = attribute.itemSize; + const newarray = attrArrays[name]; + const newMorphArrays = morphAttrsArrays[name]; + + for (let k = 0; k < itemSize; k++) { + const getterFunc = getters[k]; + newarray.push(attribute[getterFunc](index)); + + if (morphAttr) { + for (let m = 0, ml = morphAttr.length; m < ml; m++) { + newMorphArrays[m].push(morphAttr[m][getterFunc](index)); + } + } + } + } + + hashToIndex[hash] = nextIndex; + newIndices.push(nextIndex); + nextIndex++; + } + } + + // Generate typed arrays from new attribute arrays and update + // the attributeBuffers + const result = geometry.clone(); + for (let i = 0, l = attributeNames.length; i < l; i++) { + const name = attributeNames[i]; + const oldAttribute = geometry.getAttribute(name); + + const buffer = new oldAttribute.array.constructor(attrArrays[name]); + const attribute = new BufferAttribute(buffer, oldAttribute.itemSize, oldAttribute.normalized); + + result.setAttribute(name, attribute); + + // Update the attribute arrays + if (name in morphAttrsArrays) { + for (let j = 0; j < morphAttrsArrays[name].length; j++) { + const oldMorphAttribute = geometry.morphAttributes[name][j]; + + const buffer = new oldMorphAttribute.array.constructor(morphAttrsArrays[name][j]); + const morphAttribute = new BufferAttribute( + buffer, + oldMorphAttribute.itemSize, + oldMorphAttribute.normalized, + ); + result.morphAttributes[name][j] = morphAttribute; + } + } + } + + // indices + + result.setIndex(newIndices); + + return result; +} + +/** + * @param {BufferGeometry} geometry + * @param {number} drawMode + * @return {BufferGeometry>} + */ +function toTrianglesDrawMode(geometry, drawMode) { + if (drawMode === TrianglesDrawMode) { + console.warn( + "THREE.BufferGeometryUtils.toTrianglesDrawMode(): Geometry already defined as triangles.", + ); + return geometry; + } + + if (drawMode === TriangleFanDrawMode || drawMode === TriangleStripDrawMode) { + let index = geometry.getIndex(); + + // generate index if not present + + if (index === null) { + const indices = []; + + const position = geometry.getAttribute("position"); + + if (position !== undefined) { + for (let i = 0; i < position.count; i++) { + indices.push(i); + } + + geometry.setIndex(indices); + index = geometry.getIndex(); + } else { + console.error( + "THREE.BufferGeometryUtils.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.", + ); + return geometry; + } + } + + // + + const numberOfTriangles = index.count - 2; + const newIndices = []; + + if (drawMode === TriangleFanDrawMode) { + // gl.TRIANGLE_FAN + + for (let i = 1; i <= numberOfTriangles; i++) { + newIndices.push(index.getX(0)); + newIndices.push(index.getX(i)); + newIndices.push(index.getX(i + 1)); + } + } else { + // gl.TRIANGLE_STRIP + + for (let i = 0; i < numberOfTriangles; i++) { + if (i % 2 === 0) { + newIndices.push(index.getX(i)); + newIndices.push(index.getX(i + 1)); + newIndices.push(index.getX(i + 2)); + } else { + newIndices.push(index.getX(i + 2)); + newIndices.push(index.getX(i + 1)); + newIndices.push(index.getX(i)); + } + } + } + + if (newIndices.length / 3 !== numberOfTriangles) { + console.error( + "THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unable to generate correct amount of triangles.", + ); + } + + // build final geometry + + const newGeometry = geometry.clone(); + newGeometry.setIndex(newIndices); + newGeometry.clearGroups(); + + return newGeometry; + } else { + console.error("THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unknown draw mode:", drawMode); + return geometry; + } +} + +/** + * Calculates the morphed attributes of a morphed/skinned BufferGeometry. + * Helpful for Raytracing or Decals. + * @param {Mesh | Line | Points} object An instance of Mesh, Line or Points. + * @return {Object} An Object with original position/normal attributes and morphed ones. + */ +function computeMorphedAttributes(object) { + if (object.geometry.isBufferGeometry !== true) { + console.error("THREE.BufferGeometryUtils: Geometry is not of type BufferGeometry."); + return null; + } + + const _vA = new Vector3(); + const _vB = new Vector3(); + const _vC = new Vector3(); + + const _tempA = new Vector3(); + const _tempB = new Vector3(); + const _tempC = new Vector3(); + + const _morphA = new Vector3(); + const _morphB = new Vector3(); + const _morphC = new Vector3(); + + function _calculateMorphedAttributeData( + object, + material, + attribute, + morphAttribute, + morphTargetsRelative, + a, + b, + c, + modifiedAttributeArray, + ) { + _vA.fromBufferAttribute(attribute, a); + _vB.fromBufferAttribute(attribute, b); + _vC.fromBufferAttribute(attribute, c); + + const morphInfluences = object.morphTargetInfluences; + + if (material.morphTargets && morphAttribute && morphInfluences) { + _morphA.set(0, 0, 0); + _morphB.set(0, 0, 0); + _morphC.set(0, 0, 0); + + for (let i = 0, il = morphAttribute.length; i < il; i++) { + const influence = morphInfluences[i]; + const morph = morphAttribute[i]; + + if (influence === 0) continue; + + _tempA.fromBufferAttribute(morph, a); + _tempB.fromBufferAttribute(morph, b); + _tempC.fromBufferAttribute(morph, c); + + if (morphTargetsRelative) { + _morphA.addScaledVector(_tempA, influence); + _morphB.addScaledVector(_tempB, influence); + _morphC.addScaledVector(_tempC, influence); + } else { + _morphA.addScaledVector(_tempA.sub(_vA), influence); + _morphB.addScaledVector(_tempB.sub(_vB), influence); + _morphC.addScaledVector(_tempC.sub(_vC), influence); + } + } + + _vA.add(_morphA); + _vB.add(_morphB); + _vC.add(_morphC); + } + + if (object.isSkinnedMesh) { + object.boneTransform(a, _vA); + object.boneTransform(b, _vB); + object.boneTransform(c, _vC); + } + + modifiedAttributeArray[a * 3 + 0] = _vA.x; + modifiedAttributeArray[a * 3 + 1] = _vA.y; + modifiedAttributeArray[a * 3 + 2] = _vA.z; + modifiedAttributeArray[b * 3 + 0] = _vB.x; + modifiedAttributeArray[b * 3 + 1] = _vB.y; + modifiedAttributeArray[b * 3 + 2] = _vB.z; + modifiedAttributeArray[c * 3 + 0] = _vC.x; + modifiedAttributeArray[c * 3 + 1] = _vC.y; + modifiedAttributeArray[c * 3 + 2] = _vC.z; + } + + const geometry = object.geometry; + const material = object.material; + + let a, b, c; + const index = geometry.index; + const positionAttribute = geometry.attributes.position; + const morphPosition = geometry.morphAttributes.position; + const morphTargetsRelative = geometry.morphTargetsRelative; + const normalAttribute = geometry.attributes.normal; + const morphNormal = geometry.morphAttributes.position; + + const groups = geometry.groups; + const drawRange = geometry.drawRange; + let i, j, il, jl; + let group, groupMaterial; + let start, end; + + const modifiedPosition = new Float32Array(positionAttribute.count * positionAttribute.itemSize); + const modifiedNormal = new Float32Array(normalAttribute.count * normalAttribute.itemSize); + + if (index !== null) { + // indexed buffer geometry + + if (Array.isArray(material)) { + for (i = 0, il = groups.length; i < il; i++) { + group = groups[i]; + groupMaterial = material[group.materialIndex]; + + start = Math.max(group.start, drawRange.start); + end = Math.min(group.start + group.count, drawRange.start + drawRange.count); + + for (j = start, jl = end; j < jl; j += 3) { + a = index.getX(j); + b = index.getX(j + 1); + c = index.getX(j + 2); + + _calculateMorphedAttributeData( + object, + groupMaterial, + positionAttribute, + morphPosition, + morphTargetsRelative, + a, + b, + c, + modifiedPosition, + ); + + _calculateMorphedAttributeData( + object, + groupMaterial, + normalAttribute, + morphNormal, + morphTargetsRelative, + a, + b, + c, + modifiedNormal, + ); + } + } + } else { + start = Math.max(0, drawRange.start); + end = Math.min(index.count, drawRange.start + drawRange.count); + + for (i = start, il = end; i < il; i += 3) { + a = index.getX(i); + b = index.getX(i + 1); + c = index.getX(i + 2); + + _calculateMorphedAttributeData( + object, + material, + positionAttribute, + morphPosition, + morphTargetsRelative, + a, + b, + c, + modifiedPosition, + ); + + _calculateMorphedAttributeData( + object, + material, + normalAttribute, + morphNormal, + morphTargetsRelative, + a, + b, + c, + modifiedNormal, + ); + } + } + } else if (positionAttribute !== undefined) { + // non-indexed buffer geometry + + if (Array.isArray(material)) { + for (i = 0, il = groups.length; i < il; i++) { + group = groups[i]; + groupMaterial = material[group.materialIndex]; + + start = Math.max(group.start, drawRange.start); + end = Math.min(group.start + group.count, drawRange.start + drawRange.count); + + for (j = start, jl = end; j < jl; j += 3) { + a = j; + b = j + 1; + c = j + 2; + + _calculateMorphedAttributeData( + object, + groupMaterial, + positionAttribute, + morphPosition, + morphTargetsRelative, + a, + b, + c, + modifiedPosition, + ); + + _calculateMorphedAttributeData( + object, + groupMaterial, + normalAttribute, + morphNormal, + morphTargetsRelative, + a, + b, + c, + modifiedNormal, + ); + } + } + } else { + start = Math.max(0, drawRange.start); + end = Math.min(positionAttribute.count, drawRange.start + drawRange.count); + + for (i = start, il = end; i < il; i += 3) { + a = i; + b = i + 1; + c = i + 2; + + _calculateMorphedAttributeData( + object, + material, + positionAttribute, + morphPosition, + morphTargetsRelative, + a, + b, + c, + modifiedPosition, + ); + + _calculateMorphedAttributeData( + object, + material, + normalAttribute, + morphNormal, + morphTargetsRelative, + a, + b, + c, + modifiedNormal, + ); + } + } + } + + const morphedPositionAttribute = new Float32BufferAttribute(modifiedPosition, 3); + const morphedNormalAttribute = new Float32BufferAttribute(modifiedNormal, 3); + + return { + positionAttribute: positionAttribute, + normalAttribute: normalAttribute, + morphedPositionAttribute: morphedPositionAttribute, + morphedNormalAttribute: morphedNormalAttribute, + }; +} + +export { + computeTangents, + mergeBufferGeometries, + mergeBufferAttributes, + interleaveAttributes, + estimateBytesUsed, + mergeVertices, + toTrianglesDrawMode, + computeMorphedAttributes, +}; diff --git a/frontend/javascripts/libs/DRACOLoader.ts b/frontend/javascripts/libs/DRACOLoader.ts new file mode 100644 index 00000000000..5ddac61fa87 --- /dev/null +++ b/frontend/javascripts/libs/DRACOLoader.ts @@ -0,0 +1,470 @@ +// @ts-nocheck +// Copied from node_modules/three/examples/jsm/loaders/DRACOLoader.js to fix ERR_REQUIRE_ESM error. +import { BufferAttribute, BufferGeometry, FileLoader, Loader } from "three"; + +const _taskCache = new WeakMap(); + +class DRACOLoader extends Loader { + constructor(manager) { + super(manager); + + this.decoderPath = ""; + this.decoderConfig = {}; + this.decoderBinary = null; + this.decoderPending = null; + + this.workerLimit = 4; + this.workerPool = []; + this.workerNextTaskID = 1; + this.workerSourceURL = ""; + + this.defaultAttributeIDs = { + position: "POSITION", + normal: "NORMAL", + color: "COLOR", + uv: "TEX_COORD", + }; + this.defaultAttributeTypes = { + position: "Float32Array", + normal: "Float32Array", + color: "Float32Array", + uv: "Float32Array", + }; + } + + setDecoderPath(path) { + this.decoderPath = path; + + return this; + } + + setDecoderConfig(config) { + this.decoderConfig = config; + + return this; + } + + setWorkerLimit(workerLimit) { + this.workerLimit = workerLimit; + + return this; + } + + load(url, onLoad, onProgress, onError) { + const loader = new FileLoader(this.manager); + + loader.setPath(this.path); + loader.setResponseType("arraybuffer"); + loader.setRequestHeader(this.requestHeader); + loader.setWithCredentials(this.withCredentials); + + loader.load( + url, + (buffer) => { + const taskConfig = { + attributeIDs: this.defaultAttributeIDs, + attributeTypes: this.defaultAttributeTypes, + useUniqueIDs: false, + }; + + this.decodeGeometry(buffer, taskConfig).then(onLoad).catch(onError); + }, + onProgress, + onError, + ); + } + + /** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */ + decodeDracoFile(buffer, callback, attributeIDs, attributeTypes) { + const taskConfig = { + attributeIDs: attributeIDs || this.defaultAttributeIDs, + attributeTypes: attributeTypes || this.defaultAttributeTypes, + useUniqueIDs: !!attributeIDs, + }; + + this.decodeGeometry(buffer, taskConfig).then(callback); + } + + decodeGeometry(buffer, taskConfig) { + // TODO: For backward-compatibility, support 'attributeTypes' objects containing + // references (rather than names) to typed array constructors. These must be + // serialized before sending them to the worker. + for (const attribute in taskConfig.attributeTypes) { + const type = taskConfig.attributeTypes[attribute]; + + if (type.BYTES_PER_ELEMENT !== undefined) { + taskConfig.attributeTypes[attribute] = type.name; + } + } + + // + + const taskKey = JSON.stringify(taskConfig); + + // Check for an existing task using this buffer. A transferred buffer cannot be transferred + // again from this thread. + if (_taskCache.has(buffer)) { + const cachedTask = _taskCache.get(buffer); + + if (cachedTask.key === taskKey) { + return cachedTask.promise; + } else if (buffer.byteLength === 0) { + // Technically, it would be possible to wait for the previous task to complete, + // transfer the buffer back, and decode again with the second configuration. That + // is complex, and I don't know of any reason to decode a Draco buffer twice in + // different ways, so this is left unimplemented. + throw new Error( + "THREE.DRACOLoader: Unable to re-decode a buffer with different " + + "settings. Buffer has already been transferred.", + ); + } + } + + // + + let worker; + const taskID = this.workerNextTaskID++; + const taskCost = buffer.byteLength; + + // Obtain a worker and assign a task, and construct a geometry instance + // when the task completes. + const geometryPending = this._getWorker(taskID, taskCost) + .then((_worker) => { + worker = _worker; + + return new Promise((resolve, reject) => { + worker._callbacks[taskID] = { resolve, reject }; + + worker.postMessage({ type: "decode", id: taskID, taskConfig, buffer }, [buffer]); + + // this.debug(); + }); + }) + .then((message) => this._createGeometry(message.geometry)); + + // Remove task from the task list. + // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416) + geometryPending + .catch(() => true) + .then(() => { + if (worker && taskID) { + this._releaseTask(worker, taskID); + + // this.debug(); + } + }); + + // Cache the task result. + _taskCache.set(buffer, { + key: taskKey, + promise: geometryPending, + }); + + return geometryPending; + } + + _createGeometry(geometryData) { + const geometry = new BufferGeometry(); + + if (geometryData.index) { + geometry.setIndex(new BufferAttribute(geometryData.index.array, 1)); + } + + for (let i = 0; i < geometryData.attributes.length; i++) { + const attribute = geometryData.attributes[i]; + const name = attribute.name; + const array = attribute.array; + const itemSize = attribute.itemSize; + + geometry.setAttribute(name, new BufferAttribute(array, itemSize)); + } + + return geometry; + } + + _loadLibrary(url, responseType) { + const loader = new FileLoader(this.manager); + loader.setPath(this.decoderPath); + loader.setResponseType(responseType); + loader.setWithCredentials(this.withCredentials); + + return new Promise((resolve, reject) => { + loader.load(url, resolve, undefined, reject); + }); + } + + preload() { + this._initDecoder(); + + return this; + } + + _initDecoder() { + if (this.decoderPending) return this.decoderPending; + + const useJS = typeof WebAssembly !== "object" || this.decoderConfig.type === "js"; + const librariesPending = []; + + if (useJS) { + librariesPending.push(this._loadLibrary("draco_decoder.js", "text")); + } else { + librariesPending.push(this._loadLibrary("draco_wasm_wrapper.js", "text")); + librariesPending.push(this._loadLibrary("draco_decoder.wasm", "arraybuffer")); + } + + this.decoderPending = Promise.all(librariesPending).then((libraries) => { + const jsContent = libraries[0]; + + if (!useJS) { + this.decoderConfig.wasmBinary = libraries[1]; + } + + const fn = DRACOWorker.toString(); + + const body = [ + "/* draco decoder */", + jsContent, + "", + "/* worker */", + fn.substring(fn.indexOf("{") + 1, fn.lastIndexOf("}")), + ].join("\n"); + + this.workerSourceURL = URL.createObjectURL(new Blob([body])); + }); + + return this.decoderPending; + } + + _getWorker(taskID, taskCost) { + return this._initDecoder().then(() => { + if (this.workerPool.length < this.workerLimit) { + const worker = new Worker(this.workerSourceURL); + + worker._callbacks = {}; + worker._taskCosts = {}; + worker._taskLoad = 0; + + worker.postMessage({ type: "init", decoderConfig: this.decoderConfig }); + + worker.onmessage = function (e) { + const message = e.data; + + switch (message.type) { + case "decode": + worker._callbacks[message.id].resolve(message); + break; + + case "error": + worker._callbacks[message.id].reject(message); + break; + + default: + console.error('THREE.DRACOLoader: Unexpected message, "' + message.type + '"'); + } + }; + + this.workerPool.push(worker); + } else { + this.workerPool.sort(function (a, b) { + return a._taskLoad > b._taskLoad ? -1 : 1; + }); + } + + const worker = this.workerPool[this.workerPool.length - 1]; + worker._taskCosts[taskID] = taskCost; + worker._taskLoad += taskCost; + return worker; + }); + } + + _releaseTask(worker, taskID) { + worker._taskLoad -= worker._taskCosts[taskID]; + delete worker._callbacks[taskID]; + delete worker._taskCosts[taskID]; + } + + debug() { + console.log( + "Task load: ", + this.workerPool.map((worker) => worker._taskLoad), + ); + } + + dispose() { + for (let i = 0; i < this.workerPool.length; ++i) { + this.workerPool[i].terminate(); + } + + this.workerPool.length = 0; + + return this; + } +} + +/* WEB WORKER */ + +function DRACOWorker() { + let decoderConfig; + let decoderPending; + + onmessage = function (e) { + const message = e.data; + + switch (message.type) { + case "init": + decoderConfig = message.decoderConfig; + decoderPending = new Promise(function (resolve /*, reject*/) { + decoderConfig.onModuleLoaded = function (draco) { + // Module is Promise-like. Wrap before resolving to avoid loop. + resolve({ draco: draco }); + }; + + DracoDecoderModule(decoderConfig); // eslint-disable-line no-undef + }); + break; + + case "decode": + const buffer = message.buffer; + const taskConfig = message.taskConfig; + decoderPending.then((module) => { + const draco = module.draco; + const decoder = new draco.Decoder(); + const decoderBuffer = new draco.DecoderBuffer(); + decoderBuffer.Init(new Int8Array(buffer), buffer.byteLength); + + try { + const geometry = decodeGeometry(draco, decoder, decoderBuffer, taskConfig); + + const buffers = geometry.attributes.map((attr) => attr.array.buffer); + + if (geometry.index) buffers.push(geometry.index.array.buffer); + + self.postMessage({ type: "decode", id: message.id, geometry }, buffers); + } catch (error) { + console.error(error); + + self.postMessage({ type: "error", id: message.id, error: error.message }); + } finally { + draco.destroy(decoderBuffer); + draco.destroy(decoder); + } + }); + break; + } + }; + + function decodeGeometry(draco, decoder, decoderBuffer, taskConfig) { + const attributeIDs = taskConfig.attributeIDs; + const attributeTypes = taskConfig.attributeTypes; + + let dracoGeometry; + let decodingStatus; + + const geometryType = decoder.GetEncodedGeometryType(decoderBuffer); + + if (geometryType === draco.TRIANGULAR_MESH) { + dracoGeometry = new draco.Mesh(); + decodingStatus = decoder.DecodeBufferToMesh(decoderBuffer, dracoGeometry); + } else if (geometryType === draco.POINT_CLOUD) { + dracoGeometry = new draco.PointCloud(); + decodingStatus = decoder.DecodeBufferToPointCloud(decoderBuffer, dracoGeometry); + } else { + throw new Error("THREE.DRACOLoader: Unexpected geometry type."); + } + + if (!decodingStatus.ok() || dracoGeometry.ptr === 0) { + throw new Error("THREE.DRACOLoader: Decoding failed: " + decodingStatus.error_msg()); + } + + const geometry = { index: null, attributes: [] }; + + // Gather all vertex attributes. + for (const attributeName in attributeIDs) { + const attributeType = self[attributeTypes[attributeName]]; + + let attribute; + let attributeID; + + // A Draco file may be created with default vertex attributes, whose attribute IDs + // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively, + // a Draco file may contain a custom set of attributes, identified by known unique + // IDs. glTF files always do the latter, and `.drc` files typically do the former. + if (taskConfig.useUniqueIDs) { + attributeID = attributeIDs[attributeName]; + attribute = decoder.GetAttributeByUniqueId(dracoGeometry, attributeID); + } else { + attributeID = decoder.GetAttributeId(dracoGeometry, draco[attributeIDs[attributeName]]); + + if (attributeID === -1) continue; + + attribute = decoder.GetAttribute(dracoGeometry, attributeID); + } + + geometry.attributes.push( + decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute), + ); + } + + // Add index. + if (geometryType === draco.TRIANGULAR_MESH) { + geometry.index = decodeIndex(draco, decoder, dracoGeometry); + } + + draco.destroy(dracoGeometry); + + return geometry; + } + + function decodeIndex(draco, decoder, dracoGeometry) { + const numFaces = dracoGeometry.num_faces(); + const numIndices = numFaces * 3; + const byteLength = numIndices * 4; + + const ptr = draco._malloc(byteLength); + decoder.GetTrianglesUInt32Array(dracoGeometry, byteLength, ptr); + const index = new Uint32Array(draco.HEAPF32.buffer, ptr, numIndices).slice(); + draco._free(ptr); + + return { array: index, itemSize: 1 }; + } + + function decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute) { + const numComponents = attribute.num_components(); + const numPoints = dracoGeometry.num_points(); + const numValues = numPoints * numComponents; + const byteLength = numValues * attributeType.BYTES_PER_ELEMENT; + const dataType = getDracoDataType(draco, attributeType); + + const ptr = draco._malloc(byteLength); + decoder.GetAttributeDataArrayForAllPoints(dracoGeometry, attribute, dataType, byteLength, ptr); + const array = new attributeType(draco.HEAPF32.buffer, ptr, numValues).slice(); + draco._free(ptr); + + return { + name: attributeName, + array: array, + itemSize: numComponents, + }; + } + + function getDracoDataType(draco, attributeType) { + switch (attributeType) { + case Float32Array: + return draco.DT_FLOAT32; + case Int8Array: + return draco.DT_INT8; + case Int16Array: + return draco.DT_INT16; + case Int32Array: + return draco.DT_INT32; + case Uint8Array: + return draco.DT_UINT8; + case Uint16Array: + return draco.DT_UINT16; + case Uint32Array: + return draco.DT_UINT32; + } + } +} + +export { DRACOLoader }; diff --git a/frontend/javascripts/libs/draco.ts b/frontend/javascripts/libs/draco.ts index f464f632db5..09cd247b177 100644 --- a/frontend/javascripts/libs/draco.ts +++ b/frontend/javascripts/libs/draco.ts @@ -1,5 +1,5 @@ import { BufferGeometry } from "three"; -import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; +import { DRACOLoader } from "libs/DRACOLoader.js"; let _dracoLoader: CustomDRACOLoader | null; @@ -19,6 +19,7 @@ export function getDracoLoader(): CustomDRACOLoader { if (_dracoLoader) { return _dracoLoader; } + // @ts-ignore _dracoLoader = new CustomDRACOLoader(); _dracoLoader.setDecoderPath( diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 386e435e701..418a8fe1b8a 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -39,7 +39,7 @@ import constants, { import window from "libs/window"; import { setSceneController } from "oxalis/controller/scene_controller_provider"; import { getSegmentColorAsHSL } from "oxalis/model/accessors/volumetracing_accessor"; -import { mergeVertices } from "three/examples/jsm/utils/BufferGeometryUtils.js"; +import { mergeVertices } from "libs/BufferGeometryUtils"; const CUBE_COLOR = 0x999999; diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index a07078d7610..262ea1587cb 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -4,7 +4,7 @@ import { V3 } from "libs/mjs"; import { sleep } from "libs/utils"; import ErrorHandling from "libs/error_handling"; import type { APIDataLayer } from "types/api_flow_types"; -import { mergeVertices } from "three/examples/jsm/utils/BufferGeometryUtils.js"; +import { mergeVertices } from "libs/BufferGeometryUtils"; import { ResolutionInfo, From 616bf50269ac08bc5eeb6c657161fe12bcf85b0f Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 4 Oct 2022 09:32:36 +0200 Subject: [PATCH 46/63] fix linting --- frontend/javascripts/libs/BufferGeometryUtils.ts | 2 ++ frontend/javascripts/libs/DRACOLoader.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/frontend/javascripts/libs/BufferGeometryUtils.ts b/frontend/javascripts/libs/BufferGeometryUtils.ts index 7b51c73a324..f379d5ed519 100644 --- a/frontend/javascripts/libs/BufferGeometryUtils.ts +++ b/frontend/javascripts/libs/BufferGeometryUtils.ts @@ -1,4 +1,6 @@ // @ts-nocheck +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ // Copied from three/examples/js/utils/BufferGeometryUtils.js to fix ERR_REQUIRE_ESM error. import { BufferAttribute, diff --git a/frontend/javascripts/libs/DRACOLoader.ts b/frontend/javascripts/libs/DRACOLoader.ts index 5ddac61fa87..dc6b8dd7e31 100644 --- a/frontend/javascripts/libs/DRACOLoader.ts +++ b/frontend/javascripts/libs/DRACOLoader.ts @@ -1,4 +1,7 @@ // @ts-nocheck +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable eslint-comments/no-unused-disable */ +/* eslint-disable */ // Copied from node_modules/three/examples/jsm/loaders/DRACOLoader.js to fix ERR_REQUIRE_ESM error. import { BufferAttribute, BufferGeometry, FileLoader, Loader } from "three"; From 0c95492a08c6c9534bd0a2a1a240553422cbf6cf Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 4 Oct 2022 10:11:19 +0200 Subject: [PATCH 47/63] fix import --- frontend/javascripts/libs/draco.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/libs/draco.ts b/frontend/javascripts/libs/draco.ts index 09cd247b177..27531720e5c 100644 --- a/frontend/javascripts/libs/draco.ts +++ b/frontend/javascripts/libs/draco.ts @@ -1,5 +1,5 @@ import { BufferGeometry } from "three"; -import { DRACOLoader } from "libs/DRACOLoader.js"; +import { DRACOLoader } from "libs/DRACOLoader"; let _dracoLoader: CustomDRACOLoader | null; From 45182257c0e85ec154d1971766ee63c7ea0e9b3d Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 4 Oct 2022 10:45:07 +0200 Subject: [PATCH 48/63] fix linting --- frontend/javascripts/libs/DRACOLoader.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/javascripts/libs/DRACOLoader.ts b/frontend/javascripts/libs/DRACOLoader.ts index dc6b8dd7e31..cfbd3ff285c 100644 --- a/frontend/javascripts/libs/DRACOLoader.ts +++ b/frontend/javascripts/libs/DRACOLoader.ts @@ -1,6 +1,5 @@ // @ts-nocheck /* eslint-disable eslint-comments/no-unlimited-disable */ -/* eslint-disable eslint-comments/no-unused-disable */ /* eslint-disable */ // Copied from node_modules/three/examples/jsm/loaders/DRACOLoader.js to fix ERR_REQUIRE_ESM error. import { BufferAttribute, BufferGeometry, FileLoader, Loader } from "three"; @@ -322,7 +321,7 @@ function DRACOWorker() { resolve({ draco: draco }); }; - DracoDecoderModule(decoderConfig); // eslint-disable-line no-undef + DracoDecoderModule(decoderConfig); }); break; From 126f2ccfa4cb868a6f6618f74289fe03d25a544e Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 4 Oct 2022 16:41:41 +0200 Subject: [PATCH 49/63] fix error when sharing link with meshes --- .../oxalis/model/sagas/isosurface_saga.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 262ea1587cb..76dba7eb710 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -56,7 +56,10 @@ import Toast from "libs/toast"; import { getDracoLoader } from "libs/draco"; import messages from "messages"; import processTaskWithPool from "libs/task_pool"; -import { getBaseSegmentationName } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; +import { + getBaseSegmentationName, + maybeFetchMeshFiles, +} from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { UpdateSegmentAction } from "../actions/volumetracing_actions"; const MAX_RETRY_COUNT = 5; @@ -548,13 +551,18 @@ function* loadPrecomputedMeshForSegmentId( let availableChunks = null; - const meshFile = yield* select((state) => - (state.localSegmentationData[layerName].availableMeshFiles || []).find( - (file) => file.meshFileName === meshFileName, - ), + const availableMeshFiles = yield* call( + maybeFetchMeshFiles, + segmentationLayer, + dataset, + false, + false, ); + + const meshFile = availableMeshFiles.find((file) => file.meshFileName === meshFileName); if (!meshFile) { - throw new Error("Could not find requested mesh file."); + Toast.error("Could not load mesh, since the requested mesh file was not found."); + return; } const version = meshFile.formatVersion; From 9e4bd5447d5264b04abff1412d9676e86194fcb5 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 4 Oct 2022 17:06:33 +0200 Subject: [PATCH 50/63] refactor maybeFetchMeshFiles to be a saga --- frontend/javascripts/oxalis/api/api_latest.ts | 10 ++- .../model/actions/annotation_actions.ts | 42 +++++++++++++ .../oxalis/model/sagas/isosurface_saga.ts | 61 ++++++++++++++++--- .../javascripts/oxalis/view/context_menu.tsx | 10 +-- .../segments_tab/segments_view.tsx | 34 ++++++++--- .../segments_tab/segments_view_helper.tsx | 36 ----------- 6 files changed, 132 insertions(+), 61 deletions(-) diff --git a/frontend/javascripts/oxalis/api/api_latest.ts b/frontend/javascripts/oxalis/api/api_latest.ts index c2762f3df2c..24ee26839d2 100644 --- a/frontend/javascripts/oxalis/api/api_latest.ts +++ b/frontend/javascripts/oxalis/api/api_latest.ts @@ -83,7 +83,6 @@ import { loadPrecomputedMeshAction, } from "oxalis/model/actions/segmentation_actions"; import { loadAgglomerateSkeletonForSegmentId } from "oxalis/controller/combinations/segmentation_handlers"; -import { maybeFetchMeshFiles } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { overwriteAction } from "oxalis/model/helpers/overwrite_action_middleware"; import { parseNml } from "oxalis/model/helpers/nml_helpers"; import { rotate3DViewTo } from "oxalis/controller/camera_controller"; @@ -95,6 +94,7 @@ import { refreshIsosurfacesAction, updateIsosurfaceVisibilityAction, removeIsosurfaceAction, + dispatchMaybeFetchMeshFilesAsync, } from "oxalis/model/actions/annotation_actions"; import { updateUserSettingAction, @@ -1733,7 +1733,13 @@ class DataApi { const state = Store.getState(); const { dataset } = state; - const meshFiles = await maybeFetchMeshFiles(effectiveLayer, dataset, true, false); + const meshFiles = await dispatchMaybeFetchMeshFilesAsync( + Store.dispatch, + effectiveLayer, + dataset, + true, + false, + ); return meshFiles.map((meshFile) => meshFile.meshFileName); } diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.ts b/frontend/javascripts/oxalis/model/actions/annotation_actions.ts index ca6546fe6ba..20716be25c1 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.ts @@ -1,6 +1,8 @@ import type { APIAnnotation, APIAnnotationVisibility, + APIDataLayer, + APIDataset, APIMeshFile, EditableLayerProperties, LocalMeshMetaData, @@ -14,6 +16,9 @@ import type { UserBoundingBoxWithoutIdMaybe, } from "oxalis/store"; import type { Vector3 } from "oxalis/constants"; +import _ from "lodash"; +import { Dispatch } from "redux"; +import Deferred from "libs/deferred"; type InitializeAnnotationAction = ReturnType; type SetAnnotationNameAction = ReturnType; @@ -35,6 +40,7 @@ export type UpdateIsosurfaceVisibilityAction = ReturnType; export type DeleteMeshAction = ReturnType; export type CreateMeshFromBufferAction = ReturnType; +export type MaybeFetchMeshFilesAction = ReturnType; export type TriggerIsosurfaceDownloadAction = ReturnType; export type RefreshIsosurfacesAction = ReturnType; export type RefreshIsosurfaceAction = ReturnType; @@ -67,6 +73,7 @@ export type AnnotationActionTypes = | AddMeshMetadataAction | DeleteMeshAction | CreateMeshFromBufferAction + | MaybeFetchMeshFilesAction | UpdateLocalMeshMetaDataAction | UpdateIsosurfaceVisibilityAction | TriggerIsosurfaceDownloadAction @@ -228,6 +235,22 @@ export const createMeshFromBufferAction = (name: string, buffer: ArrayBuffer) => name, } as const); +export const maybeFetchMeshFilesAction = ( + segmentationLayer: APIDataLayer | null | undefined, + dataset: APIDataset, + mustRequest: boolean, + autoActivate: boolean = true, + callback: (meshes: Array) => void = _.noop, +) => + ({ + type: "MAYBE_FETCH_MESH_FILES", + segmentationLayer, + dataset, + mustRequest, + autoActivate, + callback, + } as const); + export const triggerIsosurfaceDownloadAction = (cellName: string, cellId: number) => ({ type: "TRIGGER_ISOSURFACE_DOWNLOAD", @@ -327,3 +350,22 @@ export const setOthersMayEditForAnnotationAction = (othersMayEdit: boolean) => type: "SET_OTHERS_MAY_EDIT_FOR_ANNOTATION", othersMayEdit, } as const); + +export const dispatchMaybeFetchMeshFilesAsync = async ( + dispatch: Dispatch, + segmentationLayer: APIDataLayer | null | undefined, + dataset: APIDataset, + mustRequest: boolean, + autoActivate: boolean = true, +): Promise> => { + const readyDeferred = new Deferred(); + const action = maybeFetchMeshFilesAction( + segmentationLayer, + dataset, + mustRequest, + autoActivate, + (meshes) => readyDeferred.resolve(meshes), + ); + dispatch(action); + return await readyDeferred.promise(); +}; diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 76dba7eb710..2566f501934 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -6,6 +6,7 @@ import ErrorHandling from "libs/error_handling"; import type { APIDataLayer } from "types/api_flow_types"; import { mergeVertices } from "libs/BufferGeometryUtils"; +import Store from "oxalis/store"; import { ResolutionInfo, getResolutionInfo, @@ -21,12 +22,16 @@ import type { import type { Action } from "oxalis/model/actions/actions"; import type { Vector3 } from "oxalis/constants"; import { MappingStatusEnum } from "oxalis/constants"; -import type { +import { ImportIsosurfaceFromStlAction, UpdateIsosurfaceVisibilityAction, RemoveIsosurfaceAction, RefreshIsosurfaceAction, TriggerIsosurfaceDownloadAction, + MaybeFetchMeshFilesAction, + updateMeshFileListAction, + updateCurrentMeshFileAction, + dispatchMaybeFetchMeshFilesAsync, } from "oxalis/model/actions/annotation_actions"; import { removeIsosurfaceAction, @@ -39,7 +44,13 @@ import type { Saga } from "oxalis/model/sagas/effect-generators"; import { select } from "oxalis/model/sagas/effect-generators"; import { actionChannel, takeEvery, call, take, race, put } from "typed-redux-saga"; import { stlIsosurfaceConstants } from "oxalis/view/right-border-tabs/segments_tab/segments_view"; -import { computeIsosurface, sendAnalyticsEvent, meshV0, meshV3 } from "admin/admin_rest_api"; +import { + computeIsosurface, + sendAnalyticsEvent, + meshV0, + meshV3, + getMeshfilesForDatasetLayer, +} from "admin/admin_rest_api"; import { getFlooredPosition } from "oxalis/model/accessors/flycam_accessor"; import { setImportingMeshStateAction } from "oxalis/model/actions/ui_actions"; import { zoomedAddressToAnotherZoomStepWithInfo } from "oxalis/model/helpers/position_converter"; @@ -56,10 +67,7 @@ import Toast from "libs/toast"; import { getDracoLoader } from "libs/draco"; import messages from "messages"; import processTaskWithPool from "libs/task_pool"; -import { - getBaseSegmentationName, - maybeFetchMeshFiles, -} from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; +import { getBaseSegmentationName } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { UpdateSegmentAction } from "../actions/volumetracing_actions"; const MAX_RETRY_COUNT = 5; @@ -508,6 +516,43 @@ function* _refreshIsosurfaceWithMap( * Precomputed Meshes * */ + +function* maybeFetchMeshFiles(action: MaybeFetchMeshFilesAction): Saga { + const { segmentationLayer, dataset, mustRequest, autoActivate, callback } = action; + + if (!segmentationLayer) { + callback([]); + return; + } + + const layerName = segmentationLayer.name; + const files = yield* select((state) => state.localSegmentationData[layerName].availableMeshFiles); + + // Only send new get request, if it hasn't happened before (files in store are null) + // else return the stored files (might be empty array). Or if we force a reload. + if (!files || mustRequest) { + const availableMeshFiles = yield* call( + getMeshfilesForDatasetLayer, + dataset.dataStore.url, + dataset, + getBaseSegmentationName(segmentationLayer), + ); + yield* put(updateMeshFileListAction(layerName, availableMeshFiles)); + + const currentMeshFile = yield* select( + (state) => state.localSegmentationData[layerName].currentMeshFile, + ); + if (!currentMeshFile && availableMeshFiles.length > 0 && autoActivate) { + yield* put(updateCurrentMeshFileAction(layerName, availableMeshFiles[0].meshFileName)); + } + + callback(availableMeshFiles); + return; + } + + callback(files); +} + function* loadPrecomputedMesh(action: LoadPrecomputedMeshAction) { const { cellId, seedPosition, meshFileName, layerName } = action; const layer = yield* select((state) => @@ -552,7 +597,8 @@ function* loadPrecomputedMeshForSegmentId( let availableChunks = null; const availableMeshFiles = yield* call( - maybeFetchMeshFiles, + dispatchMaybeFetchMeshFilesAsync, + Store.dispatch, segmentationLayer, dataset, false, @@ -750,6 +796,7 @@ export default function* isosurfaceSaga(): Saga { const loadPrecomputedMeshActionChannel = yield* actionChannel("LOAD_PRECOMPUTED_MESH_ACTION"); yield* take("SCENE_CONTROLLER_READY"); yield* take("WK_READY"); + yield* takeEvery("MAYBE_FETCH_MESH_FILES", maybeFetchMeshFiles); yield* takeEvery(loadAdHocMeshActionChannel, loadAdHocIsosurfaceFromAction); yield* takeEvery(loadPrecomputedMeshActionChannel, loadPrecomputedMesh); yield* takeEvery("TRIGGER_ISOSURFACE_DOWNLOAD", downloadIsosurfaceCell); diff --git a/frontend/javascripts/oxalis/view/context_menu.tsx b/frontend/javascripts/oxalis/view/context_menu.tsx index 517d6ea5cf1..53f2308bce1 100644 --- a/frontend/javascripts/oxalis/view/context_menu.tsx +++ b/frontend/javascripts/oxalis/view/context_menu.tsx @@ -35,6 +35,7 @@ import { addUserBoundingBoxAction, deleteUserBoundingBoxAction, changeUserBoundingBoxAction, + maybeFetchMeshFilesAction, } from "oxalis/model/actions/annotation_actions"; import { deleteEdgeAction, @@ -66,10 +67,7 @@ import { loadSynapsesOfAgglomerateAtPosition, } from "oxalis/controller/combinations/segmentation_handlers"; import { isBoundingBoxUsableForMinCut } from "oxalis/model/sagas/min_cut_saga"; -import { - maybeFetchMeshFiles, - withMappingActivationConfirmation, -} from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; +import { withMappingActivationConfirmation } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { maybeGetSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import { performMinCutAction, @@ -679,9 +677,7 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps): JSX.Element { const isConnectomeMappingEnabled = useSelector(hasConnectomeFile); useEffect(() => { - (async () => { - await maybeFetchMeshFiles(visibleSegmentationLayer, dataset, false); - })(); + dispatch(maybeFetchMeshFilesAction(visibleSegmentationLayer, dataset, false)); }, [visibleSegmentationLayer, dataset]); const loadPrecomputedMesh = async () => { diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index 40412e120f8..fb84aea48e3 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -12,7 +12,10 @@ import { loadAdHocMeshAction, loadPrecomputedMeshAction, } from "oxalis/model/actions/segmentation_actions"; -import { updateCurrentMeshFileAction } from "oxalis/model/actions/annotation_actions"; +import { + maybeFetchMeshFilesAction, + updateCurrentMeshFileAction, +} from "oxalis/model/actions/annotation_actions"; import { getActiveSegmentationTracing, getVisibleSegments, @@ -25,10 +28,7 @@ import { getMappingInfo, ResolutionInfo, } from "oxalis/model/accessors/dataset_accessor"; -import { - maybeFetchMeshFiles, - getBaseSegmentationName, -} from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; +import { getBaseSegmentationName } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; import { setPositionAction } from "oxalis/model/actions/flycam_actions"; import { startComputeMeshFileJob, getJobs } from "admin/admin_rest_api"; import { updateTemporarySettingAction } from "oxalis/model/actions/settings_actions"; @@ -211,7 +211,9 @@ class SegmentsView extends React.Component { }; componentDidMount() { - maybeFetchMeshFiles(this.props.visibleSegmentationLayer, this.props.dataset, false); + Store.dispatch( + maybeFetchMeshFilesAction(this.props.visibleSegmentationLayer, this.props.dataset, false), + ); if (features().jobsEnabled) { this.pollJobData(); @@ -220,7 +222,9 @@ class SegmentsView extends React.Component { componentDidUpdate(prevProps: Props) { if (prevProps.visibleSegmentationLayer !== this.props.visibleSegmentationLayer) { - maybeFetchMeshFiles(this.props.visibleSegmentationLayer, this.props.dataset, false); + Store.dispatch( + maybeFetchMeshFilesAction(this.props.visibleSegmentationLayer, this.props.dataset, false), + ); } } @@ -251,7 +255,13 @@ class SegmentsView extends React.Component { }); // maybeFetchMeshFiles will fetch the new mesh file and also activate it if no other mesh file // currently exists. - maybeFetchMeshFiles(this.props.visibleSegmentationLayer, this.props.dataset, true); + Store.dispatch( + maybeFetchMeshFilesAction( + this.props.visibleSegmentationLayer, + this.props.dataset, + true, + ), + ); break; } @@ -558,7 +568,13 @@ class SegmentsView extends React.Component { - maybeFetchMeshFiles(this.props.visibleSegmentationLayer, this.props.dataset, true) + Store.dispatch( + maybeFetchMeshFilesAction( + this.props.visibleSegmentationLayer, + this.props.dataset, + true, + ), + ) } style={{ marginLeft: 8, diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx index 721a109ae4c..d79450b3164 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx @@ -20,42 +20,6 @@ export function getBaseSegmentationName(segmentationLayer: APIDataLayer) { // @ts-expect-error ts-migrate(2339) FIXME: Property 'fallbackLayer' does not exist on type 'A... Remove this comment to see the full error message return segmentationLayer.fallbackLayer || segmentationLayer.name; } -export async function maybeFetchMeshFiles( - segmentationLayer: APIDataLayer | null | undefined, - dataset: APIDataset, - mustRequest: boolean, - autoActivate: boolean = true, -): Promise> { - if (!segmentationLayer) { - return []; - } - - const layerName = segmentationLayer.name; - const files = Store.getState().localSegmentationData[layerName].availableMeshFiles; - - // Only send new get request, if it hasn't happened before (files in store are null) - // else return the stored files (might be empty array). Or if we force a reload. - if (!files || mustRequest) { - const availableMeshFiles = await getMeshfilesForDatasetLayer( - dataset.dataStore.url, - dataset, - getBaseSegmentationName(segmentationLayer), - ); - Store.dispatch(updateMeshFileListAction(layerName, availableMeshFiles)); - - if ( - !Store.getState().localSegmentationData[layerName].currentMeshFile && - availableMeshFiles.length > 0 && - autoActivate - ) { - Store.dispatch(updateCurrentMeshFileAction(layerName, availableMeshFiles[0].meshFileName)); - } - - return availableMeshFiles; - } - - return files; -} type MappingActivationConfirmationProps = R & { mappingName: string | null | undefined; From e658ada307a387b3beafabccb740c63f0caf21bf Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 4 Oct 2022 17:36:57 +0200 Subject: [PATCH 51/63] avoid redundant fetches of mesh files for the same layer --- .../model/actions/annotation_actions.ts | 2 +- .../oxalis/model/sagas/isosurface_saga.ts | 40 ++++++++++++++----- .../segments_tab/segments_view_helper.tsx | 7 +--- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.ts b/frontend/javascripts/oxalis/model/actions/annotation_actions.ts index 20716be25c1..409eddd9778 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.ts @@ -367,5 +367,5 @@ export const dispatchMaybeFetchMeshFilesAsync = async ( (meshes) => readyDeferred.resolve(meshes), ); dispatch(action); - return await readyDeferred.promise(); + return readyDeferred.promise(); }; diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 2566f501934..84a1500556b 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -3,8 +3,9 @@ import _ from "lodash"; import { V3 } from "libs/mjs"; import { sleep } from "libs/utils"; import ErrorHandling from "libs/error_handling"; -import type { APIDataLayer } from "types/api_flow_types"; +import type { APIDataLayer, APIMeshFile } from "types/api_flow_types"; import { mergeVertices } from "libs/BufferGeometryUtils"; +import Deferred from "libs/deferred"; import Store from "oxalis/store"; import { @@ -32,8 +33,6 @@ import { updateMeshFileListAction, updateCurrentMeshFileAction, dispatchMaybeFetchMeshFilesAsync, -} from "oxalis/model/actions/annotation_actions"; -import { removeIsosurfaceAction, addAdHocIsosurfaceAction, addPrecomputedIsosurfaceAction, @@ -517,6 +516,9 @@ function* _refreshIsosurfaceWithMap( * */ +// Avoid redundant fetches of mesh files for the same layer by +// storing Deferreds per layer lazily. +const fetchDeferredsPerLayer: Record, unknown>> = {}; function* maybeFetchMeshFiles(action: MaybeFetchMeshFilesAction): Saga { const { segmentationLayer, dataset, mustRequest, autoActivate, callback } = action; @@ -526,6 +528,30 @@ function* maybeFetchMeshFiles(action: MaybeFetchMeshFilesAction): Saga { } const layerName = segmentationLayer.name; + + function* maybeActivateMeshFile(availableMeshFiles: APIMeshFile[]) { + const currentMeshFile = yield* select( + (state) => state.localSegmentationData[layerName].currentMeshFile, + ); + if (!currentMeshFile && availableMeshFiles.length > 0 && autoActivate) { + yield* put(updateCurrentMeshFileAction(layerName, availableMeshFiles[0].meshFileName)); + } + } + + // If a deferred already exists, the one can be awaited (regardless of + // whether it's finished or not) and its content used to call the callback. + // If mustRequest was set to true, a new deferred will be created which + // replaces the old one (old references to the first Deferred will still + // work and will be resolved by the corresponding saga execution). + if (fetchDeferredsPerLayer[layerName] && !mustRequest) { + const meshes = yield* call(() => fetchDeferredsPerLayer[layerName].promise()); + yield* maybeActivateMeshFile(meshes); + callback(meshes); + return; + } + const deferred = new Deferred, unknown>(); + fetchDeferredsPerLayer[layerName] = deferred; + const files = yield* select((state) => state.localSegmentationData[layerName].availableMeshFiles); // Only send new get request, if it hasn't happened before (files in store are null) @@ -538,13 +564,9 @@ function* maybeFetchMeshFiles(action: MaybeFetchMeshFilesAction): Saga { getBaseSegmentationName(segmentationLayer), ); yield* put(updateMeshFileListAction(layerName, availableMeshFiles)); + deferred.resolve(availableMeshFiles); - const currentMeshFile = yield* select( - (state) => state.localSegmentationData[layerName].currentMeshFile, - ); - if (!currentMeshFile && availableMeshFiles.length > 0 && autoActivate) { - yield* put(updateCurrentMeshFileAction(layerName, availableMeshFiles[0].meshFileName)); - } + yield* maybeActivateMeshFile(availableMeshFiles); callback(availableMeshFiles); return; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx index d79450b3164..2ccf7659df9 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx @@ -1,14 +1,9 @@ import type { ComponentType } from "react"; import React from "react"; import { Modal } from "antd"; -import { getMeshfilesForDatasetLayer } from "admin/admin_rest_api"; -import type { APIDataset, APIDataLayer, APIMeshFile } from "types/api_flow_types"; +import type { APIDataLayer } from "types/api_flow_types"; import type { ActiveMappingInfo } from "oxalis/store"; import Store from "oxalis/store"; -import { - updateMeshFileListAction, - updateCurrentMeshFileAction, -} from "oxalis/model/actions/annotation_actions"; import { MappingStatusEnum } from "oxalis/constants"; import { setMappingAction, setMappingEnabledAction } from "oxalis/model/actions/settings_actions"; import { waitForCondition } from "libs/utils"; From 286325bfd34103604e17f4d612b8a0769b8978c5 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 4 Oct 2022 19:06:54 +0200 Subject: [PATCH 52/63] add draco decoder wasm to repo and use that instead of depending on github --- frontend/javascripts/libs/draco.ts | 4 +- public/wasm/draco_decoder.wasm | Bin 0 -> 281481 bytes public/wasm/draco_wasm_wrapper.js | 104 +++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 public/wasm/draco_decoder.wasm create mode 100644 public/wasm/draco_wasm_wrapper.js diff --git a/frontend/javascripts/libs/draco.ts b/frontend/javascripts/libs/draco.ts index 27531720e5c..65189d2f57b 100644 --- a/frontend/javascripts/libs/draco.ts +++ b/frontend/javascripts/libs/draco.ts @@ -22,9 +22,7 @@ export function getDracoLoader(): CustomDRACOLoader { // @ts-ignore _dracoLoader = new CustomDRACOLoader(); - _dracoLoader.setDecoderPath( - "https://raw.githubusercontent.com/mrdoob/three.js/r145/examples/js/libs/draco/", - ); + _dracoLoader.setDecoderPath("/assets/wasm/"); _dracoLoader.setDecoderConfig({ type: "wasm" }); // The loader could theoretically be disposed like this: // _dracoLoader.dispose(); diff --git a/public/wasm/draco_decoder.wasm b/public/wasm/draco_decoder.wasm new file mode 100644 index 0000000000000000000000000000000000000000..3f0ba9ce8ee46caf5822a61235c9c271b79c9fdd GIT binary patch literal 281481 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 z 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 literal 0 HcmV?d00001 diff --git a/public/wasm/draco_wasm_wrapper.js b/public/wasm/draco_wasm_wrapper.js new file mode 100644 index 00000000000..d12278ef53e --- /dev/null +++ b/public/wasm/draco_wasm_wrapper.js @@ -0,0 +1,104 @@ +var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(f){var m=0;return function(){return m=d);)++b;if(16k?d+=String.fromCharCode(k):(k-=65536,d+=String.fromCharCode(55296|k>>10,56320|k&1023))}}else d+=String.fromCharCode(k)}return d}function X(a,c){return a?h(ca,a,c):""}function e(a,c){0=d&&(d=65536+((d&1023)<<10)|a.charCodeAt(++b)&1023);127>=d?++c:c=2047>=d?c+2:65535>=d?c+3:c+4}c=Array(c+1);b=0;d=c.length;if(0=e){var f=a.charCodeAt(++k);e=65536+((e&1023)<<10)|f&1023}if(127>=e){if(b>=d)break;c[b++]=e}else{if(2047>=e){if(b+1>=d)break;c[b++]=192|e>>6}else{if(65535>=e){if(b+2>=d)break;c[b++]=224|e>>12}else{if(b+3>=d)break;c[b++]=240|e>>18;c[b++]=128|e>>12&63}c[b++]=128|e>>6&63}c[b++]=128| +e&63}}c[b]=0}a=n.alloc(c,T);n.copy(c,T,a)}return a}function x(){throw"cannot construct a Status, no constructor in IDL";}function A(){this.ptr=Oa();u(A)[this.ptr]=this}function B(){this.ptr=Pa();u(B)[this.ptr]=this}function C(){this.ptr=Qa();u(C)[this.ptr]=this}function D(){this.ptr=Ra();u(D)[this.ptr]=this}function E(){this.ptr=Sa();u(E)[this.ptr]=this}function q(){this.ptr=Ta();u(q)[this.ptr]=this}function J(){this.ptr=Ua();u(J)[this.ptr]=this}function w(){this.ptr=Va();u(w)[this.ptr]=this}function F(){this.ptr= +Wa();u(F)[this.ptr]=this}function r(){this.ptr=Xa();u(r)[this.ptr]=this}function G(){this.ptr=Ya();u(G)[this.ptr]=this}function H(){this.ptr=Za();u(H)[this.ptr]=this}function O(){this.ptr=$a();u(O)[this.ptr]=this}function K(){this.ptr=ab();u(K)[this.ptr]=this}function g(){this.ptr=bb();u(g)[this.ptr]=this}function y(){this.ptr=cb();u(y)[this.ptr]=this}function Q(){throw"cannot construct a VoidPtr, no constructor in IDL";}function I(){this.ptr=db();u(I)[this.ptr]=this}function L(){this.ptr=eb();u(L)[this.ptr]= +this}m=m||{};var a="undefined"!==typeof m?m:{},Ga=!1,Ha=!1;a.onRuntimeInitialized=function(){Ga=!0;if(Ha&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Ha=!0;if(Ga&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(a){if("string"!==typeof a)return!1;a=a.split(".");return 2>a.length||3=a[1]?!0:0!=a[0]||10>2]},getStr:function(){return X(R.get())}, +get64:function(){var a=R.get();R.get();return a},getZero:function(){R.get()}},Ka={__cxa_allocate_exception:function(a){return ib(a)},__cxa_throw:function(a,c,b){"uncaught_exception"in ta?ta.uncaught_exceptions++:ta.uncaught_exceptions=1;throw a;},abort:function(){z()},emscripten_get_sbrk_ptr:function(){return 18416},emscripten_memcpy_big:function(a,c,b){ca.set(ca.subarray(c,c+b),a)},emscripten_resize_heap:function(a){if(2147418112= +c?e(2*c,65536):Math.min(e((3*c+2147483648)/4,65536),2147418112);a:{try{ia.grow(c-ka.byteLength+65535>>16);l(ia.buffer);var b=1;break a}catch(d){}b=void 0}return b?!0:!1},environ_get:function(a,c){var b=0;ba().forEach(function(d,e){var f=c+b;e=P[a+4*e>>2]=f;for(f=0;f>0]=d.charCodeAt(f);T[e>>0]=0;b+=d.length+1});return 0},environ_sizes_get:function(a,c){var b=ba();P[a>>2]=b.length;var d=0;b.forEach(function(a){d+=a.length+1});P[c>>2]=d;return 0},fd_close:function(a){return 0},fd_seek:function(a, +c,b,d,e){return 0},fd_write:function(a,c,b,d){try{for(var e=0,f=0;f>2],k=P[c+(8*f+4)>>2],h=0;h>2]=e;return 0}catch(ua){return"undefined"!==typeof FS&&ua instanceof FS.ErrnoError||z(ua),ua.errno}},memory:ia,setTempRet0:function(a){},table:gb},La=function(){function e(c,b){a.asm=c.exports;aa--;a.monitorRunDependencies&&a.monitorRunDependencies(aa);0==aa&&(null!==sa&&(clearInterval(sa),sa=null),ja&&(c=ja,ja=null,c()))}function c(a){e(a.instance)} +function b(a){return Ma().then(function(a){return WebAssembly.instantiate(a,d)}).then(a,function(a){Y("failed to asynchronously prepare wasm: "+a);z(a)})}var d={env:Ka,wasi_unstable:Ka};aa++;a.monitorRunDependencies&&a.monitorRunDependencies(aa);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(Na){return Y("Module.instantiateWasm callback failed with error: "+Na),!1}(function(){if(da||"function"!==typeof WebAssembly.instantiateStreaming||va(U)||"function"!==typeof fetch)return b(c);fetch(U, +{credentials:"same-origin"}).then(function(a){return WebAssembly.instantiateStreaming(a,d).then(c,function(a){Y("wasm streaming compile failed: "+a);Y("falling back to ArrayBuffer instantiation");b(c)})})})();return{}}();a.asm=La;var hb=a.___wasm_call_ctors=function(){return a.asm.__wasm_call_ctors.apply(null,arguments)},jb=a._emscripten_bind_Status_code_0=function(){return a.asm.emscripten_bind_Status_code_0.apply(null,arguments)},kb=a._emscripten_bind_Status_ok_0=function(){return a.asm.emscripten_bind_Status_ok_0.apply(null, +arguments)},lb=a._emscripten_bind_Status_error_msg_0=function(){return a.asm.emscripten_bind_Status_error_msg_0.apply(null,arguments)},mb=a._emscripten_bind_Status___destroy___0=function(){return a.asm.emscripten_bind_Status___destroy___0.apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return a.asm.emscripten_bind_DracoUInt16Array_DracoUInt16Array_0.apply(null,arguments)},nb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoUInt16Array_GetValue_1.apply(null, +arguments)},ob=a._emscripten_bind_DracoUInt16Array_size_0=function(){return a.asm.emscripten_bind_DracoUInt16Array_size_0.apply(null,arguments)},pb=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return a.asm.emscripten_bind_DracoUInt16Array___destroy___0.apply(null,arguments)},Pa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return a.asm.emscripten_bind_PointCloud_PointCloud_0.apply(null,arguments)},qb=a._emscripten_bind_PointCloud_num_attributes_0=function(){return a.asm.emscripten_bind_PointCloud_num_attributes_0.apply(null, +arguments)},rb=a._emscripten_bind_PointCloud_num_points_0=function(){return a.asm.emscripten_bind_PointCloud_num_points_0.apply(null,arguments)},sb=a._emscripten_bind_PointCloud___destroy___0=function(){return a.asm.emscripten_bind_PointCloud___destroy___0.apply(null,arguments)},Qa=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return a.asm.emscripten_bind_DracoUInt8Array_DracoUInt8Array_0.apply(null,arguments)},tb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoUInt8Array_GetValue_1.apply(null, +arguments)},ub=a._emscripten_bind_DracoUInt8Array_size_0=function(){return a.asm.emscripten_bind_DracoUInt8Array_size_0.apply(null,arguments)},vb=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return a.asm.emscripten_bind_DracoUInt8Array___destroy___0.apply(null,arguments)},Ra=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return a.asm.emscripten_bind_DracoUInt32Array_DracoUInt32Array_0.apply(null,arguments)},wb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoUInt32Array_GetValue_1.apply(null, +arguments)},xb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return a.asm.emscripten_bind_DracoUInt32Array_size_0.apply(null,arguments)},yb=a._emscripten_bind_DracoUInt32Array___destroy___0=function(){return a.asm.emscripten_bind_DracoUInt32Array___destroy___0.apply(null,arguments)},Sa=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return a.asm.emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0.apply(null,arguments)},zb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1= +function(){return a.asm.emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1.apply(null,arguments)},Ab=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return a.asm.emscripten_bind_AttributeOctahedronTransform_quantization_bits_0.apply(null,arguments)},Bb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return a.asm.emscripten_bind_AttributeOctahedronTransform___destroy___0.apply(null,arguments)},Ta=a._emscripten_bind_PointAttribute_PointAttribute_0= +function(){return a.asm.emscripten_bind_PointAttribute_PointAttribute_0.apply(null,arguments)},Cb=a._emscripten_bind_PointAttribute_size_0=function(){return a.asm.emscripten_bind_PointAttribute_size_0.apply(null,arguments)},Db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=function(){return a.asm.emscripten_bind_PointAttribute_GetAttributeTransformData_0.apply(null,arguments)},Eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return a.asm.emscripten_bind_PointAttribute_attribute_type_0.apply(null, +arguments)},Fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return a.asm.emscripten_bind_PointAttribute_data_type_0.apply(null,arguments)},Gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return a.asm.emscripten_bind_PointAttribute_num_components_0.apply(null,arguments)},Hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return a.asm.emscripten_bind_PointAttribute_normalized_0.apply(null,arguments)},Ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return a.asm.emscripten_bind_PointAttribute_byte_stride_0.apply(null, +arguments)},Jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return a.asm.emscripten_bind_PointAttribute_byte_offset_0.apply(null,arguments)},Kb=a._emscripten_bind_PointAttribute_unique_id_0=function(){return a.asm.emscripten_bind_PointAttribute_unique_id_0.apply(null,arguments)},Lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return a.asm.emscripten_bind_PointAttribute___destroy___0.apply(null,arguments)},Ua=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0= +function(){return a.asm.emscripten_bind_AttributeTransformData_AttributeTransformData_0.apply(null,arguments)},Mb=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return a.asm.emscripten_bind_AttributeTransformData_transform_type_0.apply(null,arguments)},Nb=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return a.asm.emscripten_bind_AttributeTransformData___destroy___0.apply(null,arguments)},Va=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0= +function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0.apply(null,arguments)},Ob=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1.apply(null,arguments)},Pb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_quantization_bits_0.apply(null,arguments)}, +Qb=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_min_value_1.apply(null,arguments)},Rb=a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform_range_0.apply(null,arguments)},Sb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return a.asm.emscripten_bind_AttributeQuantizationTransform___destroy___0.apply(null,arguments)}, +Wa=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return a.asm.emscripten_bind_DracoInt8Array_DracoInt8Array_0.apply(null,arguments)},Tb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoInt8Array_GetValue_1.apply(null,arguments)},Ub=a._emscripten_bind_DracoInt8Array_size_0=function(){return a.asm.emscripten_bind_DracoInt8Array_size_0.apply(null,arguments)},Vb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return a.asm.emscripten_bind_DracoInt8Array___destroy___0.apply(null, +arguments)},Xa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return a.asm.emscripten_bind_MetadataQuerier_MetadataQuerier_0.apply(null,arguments)},Wb=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return a.asm.emscripten_bind_MetadataQuerier_HasEntry_2.apply(null,arguments)},Xb=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return a.asm.emscripten_bind_MetadataQuerier_GetIntEntry_2.apply(null,arguments)},Yb=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3= +function(){return a.asm.emscripten_bind_MetadataQuerier_GetIntEntryArray_3.apply(null,arguments)},Zb=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return a.asm.emscripten_bind_MetadataQuerier_GetDoubleEntry_2.apply(null,arguments)},$b=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return a.asm.emscripten_bind_MetadataQuerier_GetStringEntry_2.apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return a.asm.emscripten_bind_MetadataQuerier_NumEntries_1.apply(null, +arguments)},bc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return a.asm.emscripten_bind_MetadataQuerier_GetEntryName_2.apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return a.asm.emscripten_bind_MetadataQuerier___destroy___0.apply(null,arguments)},Ya=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return a.asm.emscripten_bind_DracoInt16Array_DracoInt16Array_0.apply(null,arguments)},dc=a._emscripten_bind_DracoInt16Array_GetValue_1= +function(){return a.asm.emscripten_bind_DracoInt16Array_GetValue_1.apply(null,arguments)},ec=a._emscripten_bind_DracoInt16Array_size_0=function(){return a.asm.emscripten_bind_DracoInt16Array_size_0.apply(null,arguments)},fc=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return a.asm.emscripten_bind_DracoInt16Array___destroy___0.apply(null,arguments)},Za=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return a.asm.emscripten_bind_DracoFloat32Array_DracoFloat32Array_0.apply(null, +arguments)},gc=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoFloat32Array_GetValue_1.apply(null,arguments)},hc=a._emscripten_bind_DracoFloat32Array_size_0=function(){return a.asm.emscripten_bind_DracoFloat32Array_size_0.apply(null,arguments)},ic=a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return a.asm.emscripten_bind_DracoFloat32Array___destroy___0.apply(null,arguments)},$a=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return a.asm.emscripten_bind_GeometryAttribute_GeometryAttribute_0.apply(null, +arguments)},jc=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return a.asm.emscripten_bind_GeometryAttribute___destroy___0.apply(null,arguments)},ab=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=function(){return a.asm.emscripten_bind_DecoderBuffer_DecoderBuffer_0.apply(null,arguments)},kc=a._emscripten_bind_DecoderBuffer_Init_2=function(){return a.asm.emscripten_bind_DecoderBuffer_Init_2.apply(null,arguments)},lc=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return a.asm.emscripten_bind_DecoderBuffer___destroy___0.apply(null, +arguments)},bb=a._emscripten_bind_Decoder_Decoder_0=function(){return a.asm.emscripten_bind_Decoder_Decoder_0.apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetEncodedGeometryType_1=function(){return a.asm.emscripten_bind_Decoder_GetEncodedGeometryType_1.apply(null,arguments)},nc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return a.asm.emscripten_bind_Decoder_DecodeBufferToPointCloud_2.apply(null,arguments)},oc=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return a.asm.emscripten_bind_Decoder_DecodeBufferToMesh_2.apply(null, +arguments)},pc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return a.asm.emscripten_bind_Decoder_GetAttributeId_2.apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return a.asm.emscripten_bind_Decoder_GetAttributeIdByName_2.apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3.apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetAttribute_2= +function(){return a.asm.emscripten_bind_Decoder_GetAttribute_2.apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return a.asm.emscripten_bind_Decoder_GetAttributeByUniqueId_2.apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return a.asm.emscripten_bind_Decoder_GetMetadata_1.apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return a.asm.emscripten_bind_Decoder_GetAttributeMetadata_2.apply(null, +arguments)},wc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=function(){return a.asm.emscripten_bind_Decoder_GetFaceFromMesh_3.apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return a.asm.emscripten_bind_Decoder_GetTriangleStripsFromMesh_2.apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return a.asm.emscripten_bind_Decoder_GetTrianglesUInt16Array_3.apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3= +function(){return a.asm.emscripten_bind_Decoder_GetTrianglesUInt32Array_3.apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeFloat_3.apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3.apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeIntForAllPoints_3.apply(null, +arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3.apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3.apply(null,arguments)},Fc=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3.apply(null,arguments)}, +Gc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3.apply(null,arguments)},Hc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3.apply(null,arguments)},Ic=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return a.asm.emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3.apply(null,arguments)},Jc= +a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return a.asm.emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5.apply(null,arguments)},Kc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return a.asm.emscripten_bind_Decoder_SkipAttributeTransform_1.apply(null,arguments)},Lc=a._emscripten_bind_Decoder___destroy___0=function(){return a.asm.emscripten_bind_Decoder___destroy___0.apply(null,arguments)},cb=a._emscripten_bind_Mesh_Mesh_0=function(){return a.asm.emscripten_bind_Mesh_Mesh_0.apply(null, +arguments)},Mc=a._emscripten_bind_Mesh_num_faces_0=function(){return a.asm.emscripten_bind_Mesh_num_faces_0.apply(null,arguments)},Nc=a._emscripten_bind_Mesh_num_attributes_0=function(){return a.asm.emscripten_bind_Mesh_num_attributes_0.apply(null,arguments)},Oc=a._emscripten_bind_Mesh_num_points_0=function(){return a.asm.emscripten_bind_Mesh_num_points_0.apply(null,arguments)},Pc=a._emscripten_bind_Mesh___destroy___0=function(){return a.asm.emscripten_bind_Mesh___destroy___0.apply(null,arguments)}, +Qc=a._emscripten_bind_VoidPtr___destroy___0=function(){return a.asm.emscripten_bind_VoidPtr___destroy___0.apply(null,arguments)},db=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=function(){return a.asm.emscripten_bind_DracoInt32Array_DracoInt32Array_0.apply(null,arguments)},Rc=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return a.asm.emscripten_bind_DracoInt32Array_GetValue_1.apply(null,arguments)},Sc=a._emscripten_bind_DracoInt32Array_size_0=function(){return a.asm.emscripten_bind_DracoInt32Array_size_0.apply(null, +arguments)},Tc=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return a.asm.emscripten_bind_DracoInt32Array___destroy___0.apply(null,arguments)},eb=a._emscripten_bind_Metadata_Metadata_0=function(){return a.asm.emscripten_bind_Metadata_Metadata_0.apply(null,arguments)},Uc=a._emscripten_bind_Metadata___destroy___0=function(){return a.asm.emscripten_bind_Metadata___destroy___0.apply(null,arguments)},Vc=a._emscripten_enum_draco_StatusCode_OK=function(){return a.asm.emscripten_enum_draco_StatusCode_OK.apply(null, +arguments)},Wc=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return a.asm.emscripten_enum_draco_StatusCode_DRACO_ERROR.apply(null,arguments)},Xc=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return a.asm.emscripten_enum_draco_StatusCode_IO_ERROR.apply(null,arguments)},Yc=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=function(){return a.asm.emscripten_enum_draco_StatusCode_INVALID_PARAMETER.apply(null,arguments)},Zc=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION= +function(){return a.asm.emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION.apply(null,arguments)},$c=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return a.asm.emscripten_enum_draco_StatusCode_UNKNOWN_VERSION.apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return a.asm.emscripten_enum_draco_DataType_DT_INVALID.apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT8=function(){return a.asm.emscripten_enum_draco_DataType_DT_INT8.apply(null, +arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT8=function(){return a.asm.emscripten_enum_draco_DataType_DT_UINT8.apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT16=function(){return a.asm.emscripten_enum_draco_DataType_DT_INT16.apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return a.asm.emscripten_enum_draco_DataType_DT_UINT16.apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return a.asm.emscripten_enum_draco_DataType_DT_INT32.apply(null, +arguments)},gd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return a.asm.emscripten_enum_draco_DataType_DT_UINT32.apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return a.asm.emscripten_enum_draco_DataType_DT_INT64.apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return a.asm.emscripten_enum_draco_DataType_DT_UINT64.apply(null,arguments)},jd=a._emscripten_enum_draco_DataType_DT_FLOAT32=function(){return a.asm.emscripten_enum_draco_DataType_DT_FLOAT32.apply(null, +arguments)},kd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return a.asm.emscripten_enum_draco_DataType_DT_FLOAT64.apply(null,arguments)},ld=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return a.asm.emscripten_enum_draco_DataType_DT_BOOL.apply(null,arguments)},md=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return a.asm.emscripten_enum_draco_DataType_DT_TYPES_COUNT.apply(null,arguments)},nd=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return a.asm.emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE.apply(null, +arguments)},od=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return a.asm.emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD.apply(null,arguments)},pd=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return a.asm.emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH.apply(null,arguments)},qd=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=function(){return a.asm.emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM.apply(null, +arguments)},rd=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return a.asm.emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM.apply(null,arguments)},sd=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return a.asm.emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM.apply(null,arguments)},td=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return a.asm.emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM.apply(null, +arguments)},ud=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_INVALID.apply(null,arguments)},vd=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_POSITION.apply(null,arguments)},wd=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_NORMAL.apply(null,arguments)},xd=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR= +function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_COLOR.apply(null,arguments)},yd=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD.apply(null,arguments)},zd=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return a.asm.emscripten_enum_draco_GeometryAttribute_Type_GENERIC.apply(null,arguments)};a._setThrew=function(){return a.asm.setThrew.apply(null,arguments)};var ta=a.__ZSt18uncaught_exceptionv= +function(){return a.asm._ZSt18uncaught_exceptionv.apply(null,arguments)};a._free=function(){return a.asm.free.apply(null,arguments)};var ib=a._malloc=function(){return a.asm.malloc.apply(null,arguments)};a.stackSave=function(){return a.asm.stackSave.apply(null,arguments)};a.stackAlloc=function(){return a.asm.stackAlloc.apply(null,arguments)};a.stackRestore=function(){return a.asm.stackRestore.apply(null,arguments)};a.__growWasmMemory=function(){return a.asm.__growWasmMemory.apply(null,arguments)}; +a.dynCall_ii=function(){return a.asm.dynCall_ii.apply(null,arguments)};a.dynCall_vi=function(){return a.asm.dynCall_vi.apply(null,arguments)};a.dynCall_iii=function(){return a.asm.dynCall_iii.apply(null,arguments)};a.dynCall_vii=function(){return a.asm.dynCall_vii.apply(null,arguments)};a.dynCall_iiii=function(){return a.asm.dynCall_iiii.apply(null,arguments)};a.dynCall_v=function(){return a.asm.dynCall_v.apply(null,arguments)};a.dynCall_viii=function(){return a.asm.dynCall_viii.apply(null,arguments)}; +a.dynCall_viiii=function(){return a.asm.dynCall_viiii.apply(null,arguments)};a.dynCall_iiiiiii=function(){return a.asm.dynCall_iiiiiii.apply(null,arguments)};a.dynCall_iidiiii=function(){return a.asm.dynCall_iidiiii.apply(null,arguments)};a.dynCall_jiji=function(){return a.asm.dynCall_jiji.apply(null,arguments)};a.dynCall_viiiiii=function(){return a.asm.dynCall_viiiiii.apply(null,arguments)};a.dynCall_viiiii=function(){return a.asm.dynCall_viiiii.apply(null,arguments)};a.asm=La;var fa;a.then=function(e){if(fa)e(a); +else{var c=a.onRuntimeInitialized;a.onRuntimeInitialized=function(){c&&c();e(a)}}return a};ja=function c(){fa||ma();fa||(ja=c)};a.run=ma;if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0=n.size?(t(0>=1;break;case 4:d>>=2;break;case 8:d>>=3}for(var c=0;c Date: Wed, 5 Oct 2022 10:54:39 +0200 Subject: [PATCH 53/63] integrate some PR feedback --- frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts | 6 +++--- public/wasm/draco_wasm_wrapper.js | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 84a1500556b..8e04453dead 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -544,9 +544,9 @@ function* maybeFetchMeshFiles(action: MaybeFetchMeshFilesAction): Saga { // replaces the old one (old references to the first Deferred will still // work and will be resolved by the corresponding saga execution). if (fetchDeferredsPerLayer[layerName] && !mustRequest) { - const meshes = yield* call(() => fetchDeferredsPerLayer[layerName].promise()); - yield* maybeActivateMeshFile(meshes); - callback(meshes); + const availableMeshFiles = yield* call(() => fetchDeferredsPerLayer[layerName].promise()); + yield* maybeActivateMeshFile(availableMeshFiles); + callback(availableMeshFiles); return; } const deferred = new Deferred, unknown>(); diff --git a/public/wasm/draco_wasm_wrapper.js b/public/wasm/draco_wasm_wrapper.js index d12278ef53e..60a3b94fdda 100644 --- a/public/wasm/draco_wasm_wrapper.js +++ b/public/wasm/draco_wasm_wrapper.js @@ -1,3 +1,4 @@ +// copied from node_modules/three/examples/js/libs/draco/ var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(f){var m=0;return function(){return m Date: Wed, 5 Oct 2022 11:37:16 +0200 Subject: [PATCH 54/63] simplify maybeFetchMeshFiles saga --- .../oxalis/model/sagas/isosurface_saga.ts | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 8e04453dead..3fefa8d5527 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -538,41 +538,35 @@ function* maybeFetchMeshFiles(action: MaybeFetchMeshFilesAction): Saga { } } - // If a deferred already exists, the one can be awaited (regardless of - // whether it's finished or not) and its content used to call the callback. - // If mustRequest was set to true, a new deferred will be created which - // replaces the old one (old references to the first Deferred will still - // work and will be resolved by the corresponding saga execution). + // If a deferred already exists (and mustRequest is not true), the deferred + // can be awaited (regardless of whether it's finished or not) and its + // content used to call the callback. if (fetchDeferredsPerLayer[layerName] && !mustRequest) { const availableMeshFiles = yield* call(() => fetchDeferredsPerLayer[layerName].promise()); yield* maybeActivateMeshFile(availableMeshFiles); callback(availableMeshFiles); return; } + // A request has to be made (either because none was made before or because + // it is enforced by mustRequest). + // If mustRequest is true and an old deferred exists, a new deferred will be created which + // replaces the old one (old references to the first Deferred will still + // work and will be resolved by the corresponding saga execution). const deferred = new Deferred, unknown>(); fetchDeferredsPerLayer[layerName] = deferred; - const files = yield* select((state) => state.localSegmentationData[layerName].availableMeshFiles); - - // Only send new get request, if it hasn't happened before (files in store are null) - // else return the stored files (might be empty array). Or if we force a reload. - if (!files || mustRequest) { - const availableMeshFiles = yield* call( - getMeshfilesForDatasetLayer, - dataset.dataStore.url, - dataset, - getBaseSegmentationName(segmentationLayer), - ); - yield* put(updateMeshFileListAction(layerName, availableMeshFiles)); - deferred.resolve(availableMeshFiles); - - yield* maybeActivateMeshFile(availableMeshFiles); + const availableMeshFiles = yield* call( + getMeshfilesForDatasetLayer, + dataset.dataStore.url, + dataset, + getBaseSegmentationName(segmentationLayer), + ); + yield* put(updateMeshFileListAction(layerName, availableMeshFiles)); + deferred.resolve(availableMeshFiles); - callback(availableMeshFiles); - return; - } + yield* maybeActivateMeshFile(availableMeshFiles); - callback(files); + callback(availableMeshFiles); } function* loadPrecomputedMesh(action: LoadPrecomputedMeshAction) { From fb33b560d4f3c6e66b5ec31c1146bb9a490b2784 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 14:04:56 +0200 Subject: [PATCH 55/63] clean up version handling --- .../javascripts/oxalis/model/sagas/isosurface_saga.ts | 9 ++++++--- frontend/javascripts/types/api_flow_types.ts | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 3fefa8d5527..62e89f99218 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -629,9 +629,7 @@ function* loadPrecomputedMeshForSegmentId( const version = meshFile.formatVersion; try { - // todo: should actually check against 3, but some new mesh files - // still have version 2 ? - if (version >= 2) { + if (version >= 3) { const segmentInfo = yield* call( meshV3.getMeshfileChunksForSegment, dataset.dataStore.url, @@ -641,6 +639,11 @@ function* loadPrecomputedMeshForSegmentId( id, ); availableChunks = _.first(segmentInfo.chunks.lods)?.chunks || []; + } else if (version > 0) { + Toast.error( + `Mesh file with invalid version found (version=${version}). Recompute the mesh file.`, + ); + return; } else { availableChunks = yield* call( meshV0.getMeshfileChunksForSegment, diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts index 8754552544d..d9eb8e495ab 100644 --- a/frontend/javascripts/types/api_flow_types.ts +++ b/frontend/javascripts/types/api_flow_types.ts @@ -694,8 +694,8 @@ export type APIMeshFile = { meshFileName: string; mappingName?: string | null | undefined; // 0 - is the first mesh file version - // 1-2 - skipped for consistency with VX artifact versioning - // 3 - (some artifacts might have used 2, too) is the newer version with draco encoding. + // 1-2 - skipped/not used for consistency with VX artifact versioning + // 3 - is the newer version with draco encoding. formatVersion: number; }; export type APIConnectomeFile = { From e8902f5a564f9bb03c4679bd4fdfd48dff6bd851 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 15:14:11 +0200 Subject: [PATCH 56/63] read artifact_schema_version from mesh files instead of version property (the latter won't exist for newer mesh files, as it was redundant) --- .../webknossos/datastore/services/MeshFileService.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala index f9c4fea07db..e8806c048b3 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -211,7 +211,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC def mappingVersionForMeshFile(meshFilePath: Path): Long = executeWithCachedHdf5(meshFilePath, meshFileCache) { cachedMeshFile => - cachedMeshFile.reader.int64().getAttr("/", "version") + cachedMeshFile.reader.int64().getAttr("/", "artifact_schema_version") }.toOption.getOrElse(0) def listMeshChunksForSegmentV0(organizationName: String, From 122b1bd3b2115724a4cbb3075dba8936b366dd15 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 15:23:25 +0200 Subject: [PATCH 57/63] re-add support for v1 and v2 mesh files --- frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts | 5 ----- frontend/javascripts/types/api_flow_types.ts | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 62e89f99218..8be037f49b1 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -639,11 +639,6 @@ function* loadPrecomputedMeshForSegmentId( id, ); availableChunks = _.first(segmentInfo.chunks.lods)?.chunks || []; - } else if (version > 0) { - Toast.error( - `Mesh file with invalid version found (version=${version}). Recompute the mesh file.`, - ); - return; } else { availableChunks = yield* call( meshV0.getMeshfileChunksForSegment, diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts index d9eb8e495ab..cb6c5a59673 100644 --- a/frontend/javascripts/types/api_flow_types.ts +++ b/frontend/javascripts/types/api_flow_types.ts @@ -694,7 +694,7 @@ export type APIMeshFile = { meshFileName: string; mappingName?: string | null | undefined; // 0 - is the first mesh file version - // 1-2 - skipped/not used for consistency with VX artifact versioning + // 1-2 - the format should behave as v0 (refer to voxelytics for actual differences) // 3 - is the newer version with draco encoding. formatVersion: number; }; From 2c2e23a53dee992b3cfb1ca7b34123acd387a26e Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 15:38:10 +0200 Subject: [PATCH 58/63] clean up lighting --- .../oxalis/controller/scene_controller.ts | 89 ++++++------------- 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 418a8fe1b8a..39af8ac1880 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -306,69 +306,36 @@ class SceneController { } addLights(): void { - // At the moment, we only attach an AmbientLight for the isosurfaces group. - // The PlaneView attaches a directional light directly to the TD camera, + // Note that the PlaneView also attaches a directional light directly to the TD camera, // so that the light moves along the cam. - // const ambientLightForIsosurfaces = new THREE.AmbientLight(0x404040, 15); // soft white light - // this.isosurfacesRootGroup.add(ambientLightForIsosurfaces); - - // const ambientLightForMeshes = new THREE.AmbientLight(0x404040, 5); // soft white light - // this.meshesRootGroup.add(ambientLightForMeshes); - - let unsubscribe = () => {}; - const testLights = (a: number, b: number, c: number, d: number) => { - unsubscribe(); - const ambientLight = new THREE.AmbientLight(2105376, a); - - const directionalLight = new THREE.DirectionalLight(16777215, b); - directionalLight.position.x = 1; - directionalLight.position.y = 1; - directionalLight.position.z = 1; - directionalLight.position.normalize(); - - // const max = 10000000000; - - const directionalLight2 = new THREE.DirectionalLight(16777215, c); - directionalLight2.position.x = -1; - directionalLight2.position.y = -1; - directionalLight2.position.z = -1; - directionalLight2.position.normalize(); - - const pointLight = new THREE.PointLight(16777215, d); - pointLight.position.x = 0; - pointLight.position.y = -25; - pointLight.position.z = 10; - - this.isosurfacesRootGroup.add(ambientLight); - this.isosurfacesRootGroup.add(directionalLight); - this.isosurfacesRootGroup.add(directionalLight2); - this.isosurfacesRootGroup.add(pointLight); - - unsubscribe = () => { - this.isosurfacesRootGroup.remove(ambientLight); - this.isosurfacesRootGroup.remove(directionalLight); - this.isosurfacesRootGroup.remove(directionalLight2); - this.isosurfacesRootGroup.remove(pointLight); - }; - }; - testLights(30, 5, 5, 5); - // @ts-ignore - window.testLights = testLights; - // directionalLight.position.x = 2 * max; - // directionalLight.position.y = 0; - // directionalLight.position.z = 2 * max; - // pointLight.position.x = max / 2; - // pointLight.position.y = max / 2; - // pointLight.position.z = 2 * max; - - // const light1 = new THREE.DirectionalLight(0xefefff, 12); - // light1.position.set(1, 1, 1).normalize(); - // this.isosurfacesRootGroup.add(light1); - - // const light2 = new THREE.DirectionalLight(0xffefef, 8); - // light2.position.set(-1, -1, -1).normalize(); - // this.isosurfacesRootGroup.add(light2); + const AMBIENT_INTENSITY = 30; + const DIRECTIONAL_INTENSITRY = 5; + const POINT_INTENSITY = 5; + + const ambientLight = new THREE.AmbientLight(2105376, AMBIENT_INTENSITY); + + const directionalLight = new THREE.DirectionalLight(16777215, DIRECTIONAL_INTENSITRY); + directionalLight.position.x = 1; + directionalLight.position.y = 1; + directionalLight.position.z = 1; + directionalLight.position.normalize(); + + const directionalLight2 = new THREE.DirectionalLight(16777215, DIRECTIONAL_INTENSITRY); + directionalLight2.position.x = -1; + directionalLight2.position.y = -1; + directionalLight2.position.z = -1; + directionalLight2.position.normalize(); + + const pointLight = new THREE.PointLight(16777215, POINT_INTENSITY); + pointLight.position.x = 0; + pointLight.position.y = -25; + pointLight.position.z = 10; + + this.isosurfacesRootGroup.add(ambientLight); + this.isosurfacesRootGroup.add(directionalLight); + this.isosurfacesRootGroup.add(directionalLight2); + this.isosurfacesRootGroup.add(pointLight); } removeSTL(id: string): void { From 378b29a957c6b20ff890df9d8a18f02c517289fb Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 15:59:28 +0200 Subject: [PATCH 59/63] fix enabling of corresponding mesh file when loading meshes specified in sharing link --- .../javascripts/oxalis/model/sagas/isosurface_saga.ts | 4 +++- frontend/javascripts/oxalis/model_initialization.ts | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index 8be037f49b1..ce65dec30bc 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -808,9 +808,11 @@ export default function* isosurfaceSaga(): Saga { // Buffer actions since they might be dispatched before WK_READY const loadAdHocMeshActionChannel = yield* actionChannel("LOAD_AD_HOC_MESH_ACTION"); const loadPrecomputedMeshActionChannel = yield* actionChannel("LOAD_PRECOMPUTED_MESH_ACTION"); + const maybeFetchMeshFilesActionChannel = yield* actionChannel("MAYBE_FETCH_MESH_FILES"); + yield* take("SCENE_CONTROLLER_READY"); yield* take("WK_READY"); - yield* takeEvery("MAYBE_FETCH_MESH_FILES", maybeFetchMeshFiles); + yield* takeEvery(maybeFetchMeshFilesActionChannel, maybeFetchMeshFiles); yield* takeEvery(loadAdHocMeshActionChannel, loadAdHocIsosurfaceFromAction); yield* takeEvery(loadPrecomputedMeshActionChannel, loadPrecomputedMesh); yield* takeEvery("TRIGGER_ISOSURFACE_DOWNLOAD", downloadIsosurfaceCell); diff --git a/frontend/javascripts/oxalis/model_initialization.ts b/frontend/javascripts/oxalis/model_initialization.ts index 2cce1012406..25a7d9aff4f 100644 --- a/frontend/javascripts/oxalis/model_initialization.ts +++ b/frontend/javascripts/oxalis/model_initialization.ts @@ -29,6 +29,7 @@ import { isSegmentationLayer, getSegmentationLayers, getSegmentationLayerByNameOrFallbackName, + getSegmentationLayerByName, } from "oxalis/model/accessors/dataset_accessor"; import { getNullableSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor"; import { getServerVolumeTracings } from "oxalis/model/accessors/volumetracing_accessor"; @@ -45,6 +46,7 @@ import { getAnnotationCompoundInformation, } from "admin/admin_rest_api"; import { + dispatchMaybeFetchMeshFilesAsync, initializeAnnotationAction, updateCurrentMeshFileAction, } from "oxalis/model/actions/annotation_actions"; @@ -660,13 +662,13 @@ export function applyState(state: PartialUrlManagerState, ignoreZoom: boolean = } } -function applyLayerState(stateByLayer: UrlStateByLayer) { +async function applyLayerState(stateByLayer: UrlStateByLayer) { for (const layerName of Object.keys(stateByLayer)) { const layerState = stateByLayer[layerName]; let effectiveLayerName; + const { dataset } = Store.getState(); try { - const { dataset } = Store.getState(); // The name of the layer could have changed if a volume tracing was created from a viewed annotation effectiveLayerName = getSegmentationLayerByNameOrFallbackName(dataset, layerName).name; } catch (e) { @@ -715,6 +717,9 @@ function applyLayerState(stateByLayer: UrlStateByLayer) { const { meshFileName: currentMeshFileName, meshes } = layerState.meshInfo; if (currentMeshFileName != null) { + // ensure mesh files are loaded, so that the given mesh file name can be activated + const segmentationLayer = getSegmentationLayerByName(dataset, effectiveLayerName); + await dispatchMaybeFetchMeshFilesAsync(Store.dispatch, segmentationLayer, dataset, false); Store.dispatch(updateCurrentMeshFileAction(effectiveLayerName, currentMeshFileName)); } From 6ef6faf0baa24ae75f4ccf40b2be50091c6f1566 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 16:04:08 +0200 Subject: [PATCH 60/63] fix linting --- frontend/javascripts/oxalis/model_initialization.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model_initialization.ts b/frontend/javascripts/oxalis/model_initialization.ts index 25a7d9aff4f..557ed06baf3 100644 --- a/frontend/javascripts/oxalis/model_initialization.ts +++ b/frontend/javascripts/oxalis/model_initialization.ts @@ -717,8 +717,11 @@ async function applyLayerState(stateByLayer: UrlStateByLayer) { const { meshFileName: currentMeshFileName, meshes } = layerState.meshInfo; if (currentMeshFileName != null) { - // ensure mesh files are loaded, so that the given mesh file name can be activated const segmentationLayer = getSegmentationLayerByName(dataset, effectiveLayerName); + // Ensure mesh files are loaded, so that the given mesh file name can be activated. + // Doing this in a loop is fine, since it can only happen once (maximum) and there + // are not many other iterations (== layers) which are blocked by this. + // eslint-disable-next-line no-await-in-loop await dispatchMaybeFetchMeshFilesAsync(Store.dispatch, segmentationLayer, dataset, false); Store.dispatch(updateCurrentMeshFileAction(effectiveLayerName, currentMeshFileName)); } From 978b493ed3715ae38f4641a60416daf6d7bc7e70 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 16:18:14 +0200 Subject: [PATCH 61/63] set opacity to 100% for non-passive meshes --- frontend/javascripts/oxalis/controller/scene_controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 39af8ac1880..2f992d9ed09 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -217,7 +217,7 @@ class SceneController { tweenAnimation .to( { - opacity: passive ? 0.4 : 0.9, + opacity: passive ? 0.4 : 1, }, 500, ) From 466a12ff6e42a7f6baae95221f2f90c91646c297 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 16:24:01 +0200 Subject: [PATCH 62/63] fix typo --- frontend/javascripts/oxalis/controller/scene_controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/controller/scene_controller.ts b/frontend/javascripts/oxalis/controller/scene_controller.ts index 2f992d9ed09..188eeb95658 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.ts +++ b/frontend/javascripts/oxalis/controller/scene_controller.ts @@ -310,18 +310,18 @@ class SceneController { // so that the light moves along the cam. const AMBIENT_INTENSITY = 30; - const DIRECTIONAL_INTENSITRY = 5; + const DIRECTIONAL_INTENSITY = 5; const POINT_INTENSITY = 5; const ambientLight = new THREE.AmbientLight(2105376, AMBIENT_INTENSITY); - const directionalLight = new THREE.DirectionalLight(16777215, DIRECTIONAL_INTENSITRY); + const directionalLight = new THREE.DirectionalLight(16777215, DIRECTIONAL_INTENSITY); directionalLight.position.x = 1; directionalLight.position.y = 1; directionalLight.position.z = 1; directionalLight.position.normalize(); - const directionalLight2 = new THREE.DirectionalLight(16777215, DIRECTIONAL_INTENSITRY); + const directionalLight2 = new THREE.DirectionalLight(16777215, DIRECTIONAL_INTENSITY); directionalLight2.position.x = -1; directionalLight2.position.y = -1; directionalLight2.position.z = -1; From c08a8a9dc5ea3b3fb738e314cecc788c86907794 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Oct 2022 16:46:08 +0200 Subject: [PATCH 63/63] always add an isosurface object when loading ad-hoc (will simply overwrite the existing key), as this is important when an existing precomputed mesh is changed to an ad-hoc one (the same was already done when adding precomputed meshes) --- .../javascripts/oxalis/model/sagas/isosurface_saga.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts index ce65dec30bc..469d8455e06 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.ts @@ -290,16 +290,7 @@ function* loadIsosurfaceWithNeighbors( const { mappingName, mappingType } = isosurfaceExtraInfo; const clippedPosition = clipPositionToCubeBoundary(position); let positionsToRequest = [clippedPosition]; - const hasIsosurface = yield* select( - (state) => - state.localSegmentationData[layer.name].isosurfaces != null && - state.localSegmentationData[layer.name].isosurfaces[segmentId] != null, - ); - - if (!hasIsosurface) { - yield* put(addAdHocIsosurfaceAction(layer.name, segmentId, position, mappingName, mappingType)); - } - + yield* put(addAdHocIsosurfaceAction(layer.name, segmentId, position, mappingName, mappingType)); yield* put(startedLoadingIsosurfaceAction(layer.name, segmentId)); while (positionsToRequest.length > 0) {