diff --git a/.circleci/config.yml b/.circleci/config.yml index c37789c1b4f..4a55f667750 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,9 @@ version: 2 jobs: build_test_deploy: machine: - image: ubuntu-1604:201903-01 + image: ubuntu-2004:202111-02 + docker_layer_caching: true + resource_class: large environment: USER_NAME: circleci USER_UID: 1001 @@ -253,7 +255,7 @@ jobs: curl -X POST -H "X-Auth-Token: $RELEASE_API_TOKEN" - https://kube.scm.io/hooks/remove/webknossos/dev/master?user=CI+%28nightly%29 + https://kubernetix.scm.io/hooks/remove/webknossos/dev/master?user=CI+%28nightly%29 - run: name: Wait 3min command: sleep 180 @@ -263,7 +265,7 @@ jobs: curl -X POST -H "X-Auth-Token: $RELEASE_API_TOKEN" - https://kube.scm.io/hooks/install/webknossos/dev/master?user=CI+%28nightly%29 + https://kubernetix.scm.io/hooks/install/webknossos/dev/master?user=CI+%28nightly%29 - run: name: Install dependencies and sleep at least 3min command: | @@ -272,7 +274,7 @@ jobs: wait - run: name: Refresh datasets - command: curl https://master.webknossos.xyz/data/triggers/checkInboxBlocking?token=secretSampleUserToken + command: curl -X POST --fail https://master.webknossos.xyz/data/triggers/checkInboxBlocking?token=$WK_AUTH_TOKEN - run: name: Run screenshot-tests command: | diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 3f24eaa51e4..5022d3e1276 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -11,26 +11,33 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released [Commits](https://github.com/scalableminds/webknossos/compare/22.02.0...HEAD) ### Added +- Viewport scale bars are now dynamically adjusted to display sensible values. [#5418](https://github.com/scalableminds/webknossos/pull/6034) - Added the option to make a segment's ID active via the right-click context menu in the segments list. [#5935](https://github.com/scalableminds/webknossos/pull/6006) - Added a button next to the histogram which adapts the contrast and brightness to the currently visible data. [#5961](https://github.com/scalableminds/webknossos/pull/5961) - Running uploads can now be cancelled. [#5958](https://github.com/scalableminds/webknossos/pull/5958) +- Added experimental min-cut feature to split a segment in a volume tracing with two seeds. [#5885](https://github.com/scalableminds/webknossos/pull/5885) +- Annotations with multiple volume layers can now be uploaded. (Note that merging multiple annotations with multiple volume layers each is not supported.) [#6028](https://github.com/scalableminds/webknossos/pull/6028) +- Decrease volume annotation download latency by using a different compression level. [#6036](https://github.com/scalableminds/webknossos/pull/6036) ### Changed - Upgraded webpack build tool to v5 and all other webpack related dependencies to their latest version. Enabled persistent caching which speeds up server restarts during development as well as production builds. [#5969](https://github.com/scalableminds/webknossos/pull/5969) - Improved stability when quickly volume-annotating large structures. [#6000](https://github.com/scalableminds/webknossos/pull/6000) - The front-end API `labelVoxels` returns a promise now which fulfills as soon as the label operation was carried out. [#5955](https://github.com/scalableminds/webknossos/pull/5955) +- Changed that webKnossos no longer tries to reach a save state where all updates are sent to the backend to be in sync with the frontend when the save is triggered by a timeout. [#5999](https://github.com/scalableminds/webknossos/pull/5999) - When changing which layers are visible in an annotation, this setting is persisted in the annotation, so when you share it, viewers will see the same visibility configuration. [#5967](https://github.com/scalableminds/webknossos/pull/5967) - Downloading public annotations is now also allowed without being authenticated. [#6001](https://github.com/scalableminds/webknossos/pull/6001) - Downloaded volume annotation layers no longer produce zero-byte zipfiles but rather a valid header-only zip file with no contents. [#6022](https://github.com/scalableminds/webknossos/pull/6022) - Changed a number of API routes from GET to POST to avoid unwanted side effects. [#6023](https://github.com/scalableminds/webknossos/pull/6023) - Removed unused datastore route `checkInbox` (use `checkInboxBlocking` instead). [#6023](https://github.com/scalableminds/webknossos/pull/6023) +- Migrated to Google Analytics 4. [#6031](https://github.com/scalableminds/webknossos/pull/6031) ### Fixed - Fixed volume-related bugs which could corrupt the volume data in certain scenarios. [#5955](https://github.com/scalableminds/webknossos/pull/5955) - Fixed the placeholder resolution computation for anisotropic layers with missing base resolutions. [#5983](https://github.com/scalableminds/webknossos/pull/5983) - Fixed a bug where ad-hoc meshes were computed for a mapping, although it was disabled. [#5982](https://github.com/scalableminds/webknossos/pull/5982) - Fixed a bug where volume annotation downloads would sometimes contain truncated zips. [#6009](https://github.com/scalableminds/webknossos/pull/6009) - +- Fixed a bug where downloaded multi-layer volume annotations would have the wrong data.zip filenames. [#6028](https://github.com/scalableminds/webknossos/pull/6028) +- Fixed a bug which could cause an error message to appear when saving. [#6052](https://github.com/scalableminds/webknossos/pull/6052) ### Removed diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md index 787aa1daaaa..ff33b046042 100644 --- a/MIGRATIONS.unreleased.md +++ b/MIGRATIONS.unreleased.md @@ -7,6 +7,7 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md). ## Unreleased [Commits](https://github.com/scalableminds/webknossos/compare/22.02.0...HEAD) +- The config field `googleAnalytics.trackingId` needs to be changed to [GA4 measurement id](https://support.google.com/analytics/answer/10089681), if used. ### Postgres Evolutions: - [081-annotation-viewconfiguration.sql](conf/evolutions/081-annotation-viewconfiguration.sql) diff --git a/app/controllers/AnnotationIOController.scala b/app/controllers/AnnotationIOController.scala index fa59758cdb6..4a8e07a3b6f 100755 --- a/app/controllers/AnnotationIOController.scala +++ b/app/controllers/AnnotationIOController.scala @@ -1,6 +1,7 @@ package controllers import java.io.{BufferedOutputStream, File, FileOutputStream} +import java.util.zip.Deflater import akka.actor.ActorSystem import akka.stream.Materializer @@ -11,7 +12,12 @@ import com.scalableminds.util.tools.{Fox, FoxImplicits, TextUtils} import com.scalableminds.webknossos.datastore.SkeletonTracing.{SkeletonTracing, SkeletonTracingOpt, SkeletonTracings} import com.scalableminds.webknossos.datastore.VolumeTracing.{VolumeTracing, VolumeTracingOpt, VolumeTracings} import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits -import com.scalableminds.webknossos.datastore.models.datasource.{AbstractSegmentationLayer, SegmentationLayer} +import com.scalableminds.webknossos.datastore.models.datasource.{ + AbstractSegmentationLayer, + DataLayerLike, + GenericDataSource, + SegmentationLayer +} import com.scalableminds.webknossos.tracingstore.tracings.TracingType import com.scalableminds.webknossos.tracingstore.tracings.volume.VolumeTracingDefaults import com.typesafe.scalalogging.LazyLogging @@ -20,8 +26,8 @@ import javax.inject.Inject import models.analytics.{AnalyticsService, DownloadAnnotationEvent, UploadAnnotationEvent} import models.annotation.AnnotationState._ import models.annotation._ -import models.annotation.nml.NmlResults.NmlParseResult -import models.annotation.nml.{NmlResults, NmlService, NmlWriter} +import models.annotation.nml.NmlResults.{NmlParseResult, NmlParseSuccess} +import models.annotation.nml.{NmlResults, NmlWriter} import models.binary.{DataSet, DataSetDAO, DataSetService} import models.organization.OrganizationDAO import models.project.ProjectDAO @@ -53,7 +59,7 @@ class AnnotationIOController @Inject()( analyticsService: AnalyticsService, sil: Silhouette[WkEnv], provider: AnnotationInformationProvider, - nmlService: NmlService)(implicit ec: ExecutionContext, val materializer: Materializer) + annotationUploadService: AnnotationUploadService)(implicit ec: ExecutionContext, val materializer: Materializer) extends Controller with FoxImplicits with ProtoGeometryImplicits @@ -64,7 +70,10 @@ class AnnotationIOController @Inject()( value = """Upload NML(s) or ZIP(s) of NML(s) to create a new explorative annotation. Expects: - - As file attachment: any number of NML files or ZIP files containing NMLs, optionally with at most one volume data ZIP referenced from an NML in a ZIP + - As file attachment: + - Any number of NML files or ZIP files containing NMLs, optionally with volume data ZIPs referenced from an NML in a ZIP + - If multiple annotations are uploaded, they are merged into one. + - This is not supported if any of the annotations has multiple volume layers. - As form parameter: createGroupForEachFile [String] should be one of "true" or "false" - If "true": in merged annotation, create tree group wrapping the trees of each file - If "false": in merged annotation, rename trees with the respective file name as prefix""", @@ -86,42 +95,35 @@ Expects: val overwritingDataSetName: Option[String] = request.body.dataParts.get("datasetName").flatMap(_.headOption) val attachedFiles = request.body.files.map(f => (f.ref.path.toFile, f.filename)) - val parsedFiles = nmlService.extractFromFiles(attachedFiles, useZipName = true, overwritingDataSetName) - val tracingsProcessed = nmlService.wrapOrPrefixTrees(parsedFiles.parseResults, shouldCreateGroupForEachFile) - - val parseSuccesses: List[NmlParseResult] = tracingsProcessed.filter(_.succeeded) + val parsedFiles = + annotationUploadService.extractFromFiles(attachedFiles, useZipName = true, overwritingDataSetName) + val parsedFilesWraped = + annotationUploadService.wrapOrPrefixTrees(parsedFiles.parseResults, shouldCreateGroupForEachFile) + val parseResultsFiltered: List[NmlParseResult] = parsedFilesWraped.filter(_.succeeded) - if (parseSuccesses.isEmpty) { + if (parseResultsFiltered.isEmpty) { returnError(parsedFiles) } else { - val (skeletonTracings, volumeTracingsWithDataLocations) = extractTracings(parseSuccesses) - val name = nameForUploaded(parseSuccesses.map(_.fileName)) - val description = descriptionForNMLs(parseSuccesses.map(_.description)) - for { - _ <- bool2Fox(skeletonTracings.nonEmpty || volumeTracingsWithDataLocations.nonEmpty) ?~> "nml.file.noFile" - dataSet <- findDataSetForUploadedAnnotations(skeletonTracings, volumeTracingsWithDataLocations.map(_._1)) + parseSuccesses <- Fox.serialCombined(parseResultsFiltered)(r => r.toSuccessBox) + name = nameForUploaded(parseResultsFiltered.map(_.fileName)) + description = descriptionForNMLs(parseResultsFiltered.map(_.description)) + _ <- assertNonEmpty(parseSuccesses) + skeletonTracings = parseSuccesses.flatMap(_.skeletonTracing) + // Create a list of volume layers for each uploaded (non-skeleton-only) annotation. + // This is what determines the merging strategy for volume layers + volumeLayersGroupedRaw = parseSuccesses.map(_.volumeLayers).filter(_.nonEmpty) + dataSet <- findDataSetForUploadedAnnotations(skeletonTracings, + volumeLayersGroupedRaw.flatten.map(_.tracing)) + volumeLayersGrouped <- adaptVolumeTracingsToFallbackLayer(volumeLayersGroupedRaw, dataSet) tracingStoreClient <- tracingStoreService.clientFor(dataSet) - mergedVolumeTracingIdOpt <- Fox.runOptional(volumeTracingsWithDataLocations.headOption) { _ => - for { - volumeTracingsAdapted <- Fox.serialCombined(volumeTracingsWithDataLocations)(v => - adaptPropertiesToFallbackLayer(v._1, dataSet)) - mergedIdOpt <- tracingStoreClient.mergeVolumeTracingsByContents( - VolumeTracings(volumeTracingsAdapted.map(v => VolumeTracingOpt(Some(v)))), - volumeTracingsWithDataLocations.map(t => parsedFiles.otherFiles.get(t._2).map(_.path.toFile)), - persistTracing = true - ) - } yield mergedIdOpt - } - mergedSkeletonTracingIdOpt <- Fox.runOptional(skeletonTracings.headOption) { _ => - tracingStoreClient.mergeSkeletonTracingsByContents( - SkeletonTracings(skeletonTracings.map(t => SkeletonTracingOpt(Some(t)))), - persistTracing = true) - } - annotationLayers <- AnnotationLayer.layersFromIds(mergedSkeletonTracingIdOpt, mergedVolumeTracingIdOpt) + mergedVolumeLayers <- mergeAndSaveVolumeLayers(volumeLayersGrouped, + tracingStoreClient, + parsedFiles.otherFiles) + mergedSkeletonLayers <- mergeAndSaveSkeletonLayers(skeletonTracings, tracingStoreClient) annotation <- annotationService.createFrom(request.identity, dataSet, - annotationLayers, + mergedSkeletonLayers ::: mergedVolumeLayers, AnnotationType.Explorational, name, description) @@ -135,6 +137,55 @@ Expects: } } + private def mergeAndSaveVolumeLayers(volumeLayersGrouped: Seq[List[UploadedVolumeLayer]], + client: WKRemoteTracingStoreClient, + otherFiles: Map[String, TemporaryFile]): Fox[List[AnnotationLayer]] = { + if (volumeLayersGrouped.isEmpty) return Fox.successful(List()) + if (volumeLayersGrouped.length > 1 && volumeLayersGrouped.exists(_.length > 1)) + return Fox.failure("Cannot merge multiple annotations that each have multiple volume layers.") + if (volumeLayersGrouped.length == 1) { // Just one annotation was uploaded, keep its layers separate + Fox.serialCombined(volumeLayersGrouped.toList.flatten) { uploadedVolumeLayer => + for { + savedTracingId <- client.saveVolumeTracing(uploadedVolumeLayer.tracing, + uploadedVolumeLayer.getDataZipFrom(otherFiles)) + } yield + AnnotationLayer( + savedTracingId, + AnnotationLayerType.Volume, + uploadedVolumeLayer.name + ) + } + } else { // Multiple annotations with volume layers (but at most one each) was uploaded merge those volume layers into one + val uploadedVolumeLayersFlat = volumeLayersGrouped.toList.flatten + for { + mergedTracingId <- client.mergeVolumeTracingsByContents( + VolumeTracings(uploadedVolumeLayersFlat.map(v => VolumeTracingOpt(Some(v.tracing)))), + uploadedVolumeLayersFlat.map(v => v.getDataZipFrom(otherFiles)), + persistTracing = true + ) + } yield + List( + AnnotationLayer( + mergedTracingId, + AnnotationLayerType.Volume, + None + )) + } + } + + private def mergeAndSaveSkeletonLayers(skeletonTracings: List[SkeletonTracing], + tracingStoreClient: WKRemoteTracingStoreClient): Fox[List[AnnotationLayer]] = { + if (skeletonTracings.isEmpty) return Fox.successful(List()) + for { + mergedTracingId <- tracingStoreClient.mergeSkeletonTracingsByContents( + SkeletonTracings(skeletonTracings.map(t => SkeletonTracingOpt(Some(t)))), + persistTracing = true) + } yield List(AnnotationLayer(mergedTracingId, AnnotationLayerType.Skeleton, None)) + } + + private def assertNonEmpty(parseSuccesses: List[NmlParseSuccess]) = + bool2Fox(parseSuccesses.exists(p => p.skeletonTracing.nonEmpty || p.volumeLayers.nonEmpty)) ?~> "nml.file.noFile" + private def findDataSetForUploadedAnnotations( skeletonTracings: List[SkeletonTracing], volumeTracings: List[VolumeTracing])(implicit mp: MessagesProvider, ctx: DBAccessContext): Fox[DataSet] = @@ -173,14 +224,6 @@ Expects: Future.successful(JsonBadRequest(Messages("nml.file.noFile"))) } - private def extractTracings( - parseSuccesses: List[NmlParseResult]): (List[SkeletonTracing], List[(VolumeTracing, String)]) = { - val tracings = parseSuccesses.flatMap(_.bothTracingOpts) - val skeletons = tracings.flatMap(_._1) - val volumes = tracings.flatMap(_._2) - (skeletons, volumes) - } - private def assertAllOnSameDataSet(skeletons: List[SkeletonTracing], volumes: List[VolumeTracing]): Fox[String] = for { dataSetName <- volumes.headOption.map(_.dataSetName).orElse(skeletons.headOption.map(_.dataSetName)).toFox @@ -197,9 +240,23 @@ Expects: } yield organizationNames.headOption } - private def adaptPropertiesToFallbackLayer(volumeTracing: VolumeTracing, dataSet: DataSet): Fox[VolumeTracing] = + private def adaptVolumeTracingsToFallbackLayer(volumeLayersGrouped: List[List[UploadedVolumeLayer]], + dataSet: DataSet): Fox[List[List[UploadedVolumeLayer]]] = for { dataSource <- dataSetService.dataSourceFor(dataSet).flatMap(_.toUsable) + allAdapted <- Fox.serialCombined(volumeLayersGrouped) { volumeLayers => + Fox.serialCombined(volumeLayers) { volumeLayer => + for { + tracingAdapted <- adaptPropertiesToFallbackLayer(volumeLayer.tracing, dataSource) + } yield volumeLayer.copy(tracing = tracingAdapted) + } + } + } yield allAdapted + + private def adaptPropertiesToFallbackLayer[T <: DataLayerLike](volumeTracing: VolumeTracing, + dataSource: GenericDataSource[T]): Fox[VolumeTracing] = + for { + _ <- Fox.successful(()) fallbackLayer = dataSource.dataLayers.flatMap { case layer: SegmentationLayer if volumeTracing.fallbackLayer contains layer.name => Some(layer) case layer: AbstractSegmentationLayer if volumeTracing.fallbackLayer contains layer.name => Some(layer) @@ -320,7 +377,8 @@ Expects: _ = fetchedVolumeLayers.zipWithIndex.map { case (volumeLayer, index) => volumeLayer.volumeDataOpt.foreach { volumeData => - val dataZipName = volumeLayer.volumeDataZipName(index, fetchedSkeletonLayers.length == 1) + val dataZipName = volumeLayer.volumeDataZipName(index, fetchedVolumeLayers.length == 1) + zipper.stream.setLevel(Deflater.BEST_SPEED) zipper.addFileFromBytes(dataZipName, volumeData) } } diff --git a/app/controllers/DataSetController.scala b/app/controllers/DataSetController.scala index f859a53b3d3..b94f48a4b04 100755 --- a/app/controllers/DataSetController.scala +++ b/app/controllers/DataSetController.scala @@ -2,7 +2,7 @@ package controllers import com.mohiva.play.silhouette.api.Silhouette import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.mvc.Filter import com.scalableminds.util.tools.DefaultConverters._ import com.scalableminds.util.tools.{Fox, JsonHelper, Math} @@ -87,7 +87,7 @@ class DataSetController @Inject()(userService: UserService, Fox.successful(a) case _ => val defaultCenterOpt = dataSet.adminViewConfiguration.flatMap(c => - c.get("position").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Point3D]))) + c.get("position").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Vec3Int]))) val defaultZoomOpt = dataSet.adminViewConfiguration.flatMap(c => c.get("zoom").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Double]))) dataSetService diff --git a/app/controllers/JobsController.scala b/app/controllers/JobsController.scala index fc3c5f1ede7..457d6485e2f 100644 --- a/app/controllers/JobsController.scala +++ b/app/controllers/JobsController.scala @@ -150,6 +150,33 @@ class JobsController @Inject()(jobDAO: JobDAO, } } + def runInferNeuronsJob(organizationName: String, + dataSetName: String, + layerName: String, + bbox: String): Action[AnyContent] = + sil.SecuredAction.async { implicit request => + log(Some(slackNotificationService.noticeFailedJobRequest)) { + for { + organization <- organizationDAO.findOneByName(organizationName) ?~> Messages("organization.notFound", + organizationName) + _ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.inferNeurons.notAllowed.organization" ~> FORBIDDEN + dataSet <- dataSetDAO.findOneByNameAndOrganization(dataSetName, organization._id) ?~> Messages( + "dataSet.notFound", + dataSetName) ~> NOT_FOUND + command = "infer_neurons" + commandArgs = Json.obj( + "organization_name" -> organizationName, + "dataset_name" -> dataSetName, + "layer_name" -> layerName, + "webknossos_token" -> RpcTokenHolder.webKnossosToken, + "bbox" -> bbox, + ) + job <- jobService.submitJob(command, commandArgs, request.identity, dataSet._dataStore) ?~> "job.couldNotRunNeuronInferral" + js <- jobService.publicWrites(job) + } yield Ok(js) + } + } + def runGlobalizeFloodfills( organizationName: String, dataSetName: String, diff --git a/app/controllers/TaskController.scala b/app/controllers/TaskController.scala index 0f6b72b6732..52e57b9aded 100755 --- a/app/controllers/TaskController.scala +++ b/app/controllers/TaskController.scala @@ -19,9 +19,8 @@ import io.swagger.annotations.{ ApiResponses } import javax.inject.Inject -import models.annotation._ +import models.annotation.{AnnotationUploadService, _} import models.annotation.nml.NmlResults.TracingBoxContainer -import models.annotation.nml.NmlService import models.project.ProjectDAO import models.task._ import models.user._ @@ -42,7 +41,7 @@ class TaskController @Inject()(taskCreationService: TaskCreationService, userService: UserService, taskDAO: TaskDAO, taskService: TaskService, - nmlService: NmlService, + nmlService: AnnotationUploadService, sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) extends Controller with ResultBox diff --git a/app/models/annotation/AnnotationService.scala b/app/models/annotation/AnnotationService.scala index dc0041a434b..918a01c56fd 100755 --- a/app/models/annotation/AnnotationService.scala +++ b/app/models/annotation/AnnotationService.scala @@ -5,13 +5,18 @@ import java.io.{BufferedOutputStream, File, FileOutputStream} import akka.actor.ActorSystem import akka.stream.Materializer import com.scalableminds.util.accesscontext.{AuthorizedAccessContext, DBAccessContext, GlobalAccessContext} -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Scale, Vector3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} import com.scalableminds.util.io.ZipIO import com.scalableminds.util.mvc.Formatter import com.scalableminds.util.tools.{BoxImplicits, Fox, FoxImplicits, TextUtils} import com.scalableminds.webknossos.datastore.SkeletonTracing._ import com.scalableminds.webknossos.datastore.VolumeTracing.{VolumeTracing, VolumeTracingOpt, VolumeTracings} -import com.scalableminds.webknossos.datastore.geometry.Color +import com.scalableminds.webknossos.datastore.geometry.{ + ColorProto, + NamedBoundingBoxProto, + Vec3DoubleProto, + Vec3IntProto +} import com.scalableminds.webknossos.datastore.helpers.{NodeDefaults, ProtoGeometryImplicits, SkeletonTracingDefaults} import com.scalableminds.webknossos.datastore.models.datasource.{ ElementClass, @@ -53,7 +58,7 @@ case class DownloadAnnotation(skeletonTracingIdOpt: Option[String], volumeTracingOpt: Option[VolumeTracing], volumeDataOpt: Option[Array[Byte]], name: String, - scaleOpt: Option[Scale], + scaleOpt: Option[Vec3Double], annotation: Annotation, user: User, taskOpt: Option[Task], @@ -62,10 +67,10 @@ case class DownloadAnnotation(skeletonTracingIdOpt: Option[String], // Used to pass duplicate properties when creating a new tracing to avoid masking them. // Uses the proto-generated geometry classes, hence the full qualifiers. case class RedundantTracingProperties( - editPosition: com.scalableminds.webknossos.datastore.geometry.Point3D, - editRotation: com.scalableminds.webknossos.datastore.geometry.Vector3D, + editPosition: Vec3IntProto, + editRotation: Vec3DoubleProto, zoomLevel: Double, - userBoundingBoxes: Seq[com.scalableminds.webknossos.datastore.geometry.NamedBoundingBox] + userBoundingBoxes: Seq[NamedBoundingBoxProto] ) class AnnotationService @Inject()( @@ -121,8 +126,8 @@ class AnnotationService @Inject()( organizationName: String, fallbackLayer: Option[SegmentationLayer], boundingBox: Option[BoundingBox] = None, - startPosition: Option[Point3D] = None, - startRotation: Option[Vector3D] = None, + startPosition: Option[Vec3Int] = None, + startRotation: Option[Vec3Double] = None, resolutionRestrictions: ResolutionRestrictions ): Fox[VolumeTracing] = { val resolutions = VolumeTracingDownsampling.resolutionsForVolumeTracing(dataSource, fallbackLayer) @@ -135,8 +140,8 @@ class AnnotationService @Inject()( boundingBoxToProto(boundingBox.getOrElse(dataSource.boundingBox)), System.currentTimeMillis(), dataSource.id.name, - point3DToProto(startPosition.getOrElse(dataSource.center)), - vector3DToProto(startRotation.getOrElse(vector3DFromProto(VolumeTracingDefaults.editRotation))), + vec3IntToProto(startPosition.getOrElse(dataSource.center)), + vec3DoubleToProto(startRotation.getOrElse(vec3DoubleFromProto(VolumeTracingDefaults.editRotation))), elementClassToProto( fallbackLayer.map(layer => layer.elementClass).getOrElse(VolumeTracingDefaults.elementClass)), fallbackLayer.map(_.name), @@ -144,7 +149,7 @@ class AnnotationService @Inject()( 0, VolumeTracingDefaults.zoomLevel, organizationName = Some(organizationName), - resolutions = resolutionsRestricted.map(point3DToProto) + resolutions = resolutionsRestricted.map(vec3IntToProto) ) } @@ -248,7 +253,7 @@ class AnnotationService @Inject()( s.editRotation, s.zoomLevel, s.userBoundingBoxes ++ s.userBoundingBox.map( - com.scalableminds.webknossos.datastore.geometry.NamedBoundingBox(0, None, None, None, _)) + com.scalableminds.webknossos.datastore.geometry.NamedBoundingBoxProto(0, None, None, None, _)) ) case Right(v) => RedundantTracingProperties( @@ -256,7 +261,7 @@ class AnnotationService @Inject()( v.editRotation, v.zoomLevel, v.userBoundingBoxes ++ v.userBoundingBox.map( - com.scalableminds.webknossos.datastore.geometry.NamedBoundingBox(0, None, None, None, _)) + com.scalableminds.webknossos.datastore.geometry.NamedBoundingBoxProto(0, None, None, None, _)) ) } @@ -430,14 +435,14 @@ class AnnotationService @Inject()( def createSkeletonTracingBase(dataSetName: String, boundingBox: Option[BoundingBox], - startPosition: Point3D, - startRotation: Vector3D): SkeletonTracing = { + startPosition: Vec3Int, + startRotation: Vec3Double): SkeletonTracing = { val initialNode = NodeDefaults.createInstance.withId(1).withPosition(startPosition).withRotation(startRotation) val initialTree = Tree( 1, Seq(initialNode), Seq.empty, - Some(Color(1, 0, 0, 1)), + Some(ColorProto(1, 0, 0, 1)), Seq(BranchPoint(initialNode.id, System.currentTimeMillis())), Seq.empty, "", @@ -458,8 +463,8 @@ class AnnotationService @Inject()( def createVolumeTracingBase(dataSetName: String, organizationId: ObjectId, boundingBox: Option[BoundingBox], - startPosition: Point3D, - startRotation: Vector3D, + startPosition: Vec3Int, + startRotation: Vec3Double, volumeShowFallbackLayer: Boolean, resolutionRestrictions: ResolutionRestrictions)(implicit ctx: DBAccessContext, m: MessagesProvider): Fox[VolumeTracing] = @@ -595,7 +600,7 @@ class AnnotationService @Inject()( private def getTracingsScalesAndNamesFor(annotations: List[Annotation], skipVolumeData: Boolean)( implicit ctx: DBAccessContext): Fox[List[List[DownloadAnnotation]]] = { - def getSingleDownloadAnnotation(annotation: Annotation, scaleOpt: Option[Scale]) = + def getSingleDownloadAnnotation(annotation: Annotation, scaleOpt: Option[Vec3Double]) = for { user <- userService.findOneById(annotation._user, useCache = true) ?~> "user.notFound" taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne) ?~> "task.notFound" diff --git a/app/models/annotation/nml/NmlService.scala b/app/models/annotation/AnnotationUploadService.scala similarity index 85% rename from app/models/annotation/nml/NmlService.scala rename to app/models/annotation/AnnotationUploadService.scala index 06d5923cf46..c709b9b4217 100644 --- a/app/models/annotation/nml/NmlService.scala +++ b/app/models/annotation/AnnotationUploadService.scala @@ -1,21 +1,28 @@ -package models.annotation.nml +package models.annotation import java.io.{File, FileInputStream, InputStream} import java.nio.file.{Files, StandardCopyOption} import com.scalableminds.util.io.ZipIO import com.scalableminds.webknossos.datastore.SkeletonTracing.{SkeletonTracing, TreeGroup} +import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing import com.typesafe.scalalogging.LazyLogging import javax.inject.Inject import models.annotation.nml.NmlResults._ +import models.annotation.nml.{NmlParser, NmlResults} import net.liftweb.common.{Box, Empty, Failure, Full} import net.liftweb.util.Helpers.tryo import play.api.i18n.MessagesProvider import play.api.libs.Files.{TemporaryFile, TemporaryFileCreator} -class NmlService @Inject()(temporaryFileCreator: TemporaryFileCreator) extends LazyLogging { +case class UploadedVolumeLayer(tracing: VolumeTracing, dataZipLocation: String, name: Option[String]) { + def getDataZipFrom(otherFiles: Map[String, TemporaryFile]): Option[File] = + otherFiles.get(dataZipLocation).map(_.path.toFile) +} + +class AnnotationUploadService @Inject()(temporaryFileCreator: TemporaryFileCreator) extends LazyLogging { - def extractFromNml(file: File, name: String, overwritingDataSetName: Option[String], isTaskUpload: Boolean)( + private def extractFromNml(file: File, name: String, overwritingDataSetName: Option[String], isTaskUpload: Boolean)( implicit m: MessagesProvider): NmlParseResult = extractFromNml(new FileInputStream(file), name, overwritingDataSetName, isTaskUpload) @@ -31,8 +38,8 @@ class NmlService @Inject()(temporaryFileCreator: TemporaryFileCreator) extends L isTaskUpload: Boolean, basePath: Option[String] = None)(implicit m: MessagesProvider): NmlParseResult = NmlParser.parse(name, inputStream, overwritingDataSetName, isTaskUpload, basePath) match { - case Full((skeletonTracing, volumeTracingWithDataLocation, description)) => - NmlParseSuccess(name, skeletonTracing, volumeTracingWithDataLocation, description) + case Full((skeletonTracing, uploadedVolumeLayers, description)) => + NmlParseSuccess(name, skeletonTracing, uploadedVolumeLayers, description) case Failure(msg, _, chain) => NmlParseFailure(name, msg + chain.map(_ => formatChain(chain)).getOrElse("")) case Empty => NmlParseEmpty(name) } @@ -75,8 +82,8 @@ class NmlService @Inject()(temporaryFileCreator: TemporaryFileCreator) extends L if (parseResults.length > 1) { parseResults.map { - case NmlParseSuccess(name, Some(skeletonTracing), volumeTracingOpt, description) => - NmlParseSuccess(name, Some(renameTrees(name, skeletonTracing)), volumeTracingOpt, description) + case NmlParseSuccess(name, Some(skeletonTracing), uploadedVolumeLayers, description) => + NmlParseSuccess(name, Some(renameTrees(name, skeletonTracing)), uploadedVolumeLayers, description) case r => r } } else { @@ -97,8 +104,8 @@ class NmlService @Inject()(temporaryFileCreator: TemporaryFileCreator) extends L } parseResults.map { - case NmlParseSuccess(name, Some(skeletonTracing), volumeTracingOpt, description) => - NmlParseSuccess(name, Some(wrapTreesInGroup(name, skeletonTracing)), volumeTracingOpt, description) + case NmlParseSuccess(name, Some(skeletonTracing), uploadedVolumeLayers, description) => + NmlParseSuccess(name, Some(wrapTreesInGroup(name, skeletonTracing)), uploadedVolumeLayers, description) case r => r } } diff --git a/app/models/annotation/nml/NmlParser.scala b/app/models/annotation/nml/NmlParser.scala index c9fd209ca86..e4d24a2a721 100755 --- a/app/models/annotation/nml/NmlParser.scala +++ b/app/models/annotation/nml/NmlParser.scala @@ -5,14 +5,14 @@ import java.io.InputStream import com.scalableminds.webknossos.datastore.models.datasource.ElementClass import com.scalableminds.webknossos.datastore.SkeletonTracing._ import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing -import com.scalableminds.webknossos.datastore.geometry.{Color, NamedBoundingBox} +import com.scalableminds.webknossos.datastore.geometry.{ColorProto, NamedBoundingBoxProto} import com.scalableminds.webknossos.tracingstore.tracings.ColorGenerator import com.scalableminds.webknossos.tracingstore.tracings.skeleton.{MultiComponentTreeSplitter, TreeValidator} -import com.scalableminds.webknossos.tracingstore.tracings.volume.Volume -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Vector3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int, Vec3Double} import com.scalableminds.util.tools.ExtendedTypes.{ExtendedDouble, ExtendedString} import com.scalableminds.webknossos.datastore.helpers.{NodeDefaults, ProtoGeometryImplicits, SkeletonTracingDefaults} import com.typesafe.scalalogging.LazyLogging +import models.annotation.UploadedVolumeLayer import net.liftweb.common.Box._ import net.liftweb.common.{Box, Empty, Failure} import play.api.i18n.{Messages, MessagesProvider} @@ -30,13 +30,12 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener private val DEFAULT_INTERPOLATION = false private val DEFAULT_TIMESTAMP = 0L - @SuppressWarnings(Array("TraversableHead")) //We check if volumes are empty before accessing the head def parse(name: String, nmlInputStream: InputStream, overwritingDataSetName: Option[String], isTaskUpload: Boolean, basePath: Option[String] = None)( - implicit m: MessagesProvider): Box[(Option[SkeletonTracing], Option[(VolumeTracing, String)], String)] = + implicit m: MessagesProvider): Box[(Option[SkeletonTracing], List[UploadedVolumeLayer], String)] = try { val data = XML.load(nmlInputStream) for { @@ -71,30 +70,31 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener logger.debug(s"Parsed NML file. Trees: ${treesSplit.size}, Volumes: ${volumes.size}") - val volumeTracingWithDataLocation = - if (volumes.isEmpty) None - else - Some( - (VolumeTracing( - None, - boundingBoxToProto(taskBoundingBox.getOrElse(BoundingBox.empty)), - timestamp, - dataSetName, - editPosition, - editRotation, - ElementClass.uint32, - volumes.head.fallbackLayer, - 0, - 0, - zoomLevel, - None, - userBoundingBoxes, - organizationName - ), - basePath.getOrElse("") + volumes.head.location) + val volumeLayers: List[UploadedVolumeLayer] = + volumes.toList.map { v => + UploadedVolumeLayer( + VolumeTracing( + None, + boundingBoxToProto(taskBoundingBox.getOrElse(BoundingBox.empty)), + timestamp, + dataSetName, + editPosition, + editRotation, + ElementClass.uint32, + v.fallbackLayerName, + 0, + 0, + zoomLevel, + None, + userBoundingBoxes, + organizationName + ), + basePath.getOrElse("") + v.dataZipPath, + v.name ) + } - val skeletonTracing = + val skeletonTracingOpt: Option[SkeletonTracing] = if (treesSplit.isEmpty) None else Some( @@ -115,7 +115,7 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener ) ) - (skeletonTracing, volumeTracingWithDataLocation, description) + (skeletonTracingOpt, volumeLayers, description) } } catch { case e: org.xml.sax.SAXParseException if e.getMessage.startsWith("Premature end of file") => @@ -146,8 +146,14 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener } yield TreeGroup(name, id, children) } - private def extractVolumes(volumeNodes: NodeSeq): immutable.Seq[Volume] = - volumeNodes.map(node => Volume(getSingleAttribute(node, "location"), getSingleAttributeOpt(node, "fallbackLayer"))) + private def extractVolumes(volumeNodes: NodeSeq): immutable.Seq[NmlVolumeTag] = + volumeNodes.map( + node => + NmlVolumeTag( + getSingleAttribute(node, "location"), + getSingleAttributeOpt(node, "fallbackLayer"), + getSingleAttributeOpt(node, "name") + )) private def parseTrees(treeNodes: NodeSeq, branchPoints: Map[Int, List[BranchPoint]], @@ -158,9 +164,9 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener .toSingleBox(Messages("nml.element.invalid", "trees")) @SuppressWarnings(Array("TraversableHead")) // We check that size == 1 before accessing head - private def parseBoundingBoxes(boundingBoxNodes: NodeSeq)(implicit m: MessagesProvider): Seq[NamedBoundingBox] = + private def parseBoundingBoxes(boundingBoxNodes: NodeSeq)(implicit m: MessagesProvider): Seq[NamedBoundingBoxProto] = if (boundingBoxNodes.size == 1 && getSingleAttribute(boundingBoxNodes.head, "id").isEmpty) { - Seq.empty ++ parseBoundingBox(boundingBoxNodes.head).map(NamedBoundingBox(0, None, None, None, _)) + Seq.empty ++ parseBoundingBox(boundingBoxNodes.head).map(NamedBoundingBoxProto(0, None, None, None, _)) } else { boundingBoxNodes.flatMap(node => { val idText = getSingleAttribute(node, "id") @@ -171,20 +177,20 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener color = parseColor(node) boundingBox <- parseBoundingBox(node) nameOpt = if (name.isEmpty) None else Some(name) - } yield NamedBoundingBox(id, nameOpt, isVisible, color, boundingBox) + } yield NamedBoundingBoxProto(id, nameOpt, isVisible, color, boundingBox) }) } private def parseTaskBoundingBox( nodes: NodeSeq, isTask: Boolean, - userBoundingBoxes: Seq[NamedBoundingBox]): Option[Either[BoundingBox, NamedBoundingBox]] = + userBoundingBoxes: Seq[NamedBoundingBoxProto]): Option[Either[BoundingBox, NamedBoundingBoxProto]] = nodes.headOption.flatMap(node => parseBoundingBox(node)).map { bb => if (isTask) { Left(bb) } else { val newId = if (userBoundingBoxes.isEmpty) 0 else userBoundingBoxes.map(_.id).max + 1 - Right(NamedBoundingBox(newId, Some("task bounding box"), None, Some(getRandomColor), bb)) + Right(NamedBoundingBoxProto(newId, Some("task bounding box"), None, Some(getRandomColor), bb)) } } @@ -196,7 +202,7 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener width <- getSingleAttribute(node, "width").toIntOpt height <- getSingleAttribute(node, "height").toIntOpt depth <- getSingleAttribute(node, "depth").toIntOpt - } yield BoundingBox(Point3D(topLeftX, topLeftY, topLeftZ), width, height, depth) + } yield BoundingBox(Vec3Int(topLeftX, topLeftY, topLeftZ), width, height, depth) private def parseDataSetName(nodes: NodeSeq): String = nodes.headOption.map(node => getSingleAttribute(node, "name")).getOrElse("") @@ -213,10 +219,10 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener private def parseTime(nodes: NodeSeq): Long = nodes.headOption.flatMap(node => getSingleAttribute(node, "ms").toLongOpt).getOrElse(DEFAULT_TIME) - private def parseEditPosition(nodes: NodeSeq): Option[Point3D] = - nodes.headOption.flatMap(parsePoint3D) + private def parseEditPosition(nodes: NodeSeq): Option[Vec3Int] = + nodes.headOption.flatMap(parseVec3Int) - private def parseEditRotation(nodes: NodeSeq): Option[Vector3D] = + private def parseEditRotation(nodes: NodeSeq): Option[Vec3Double] = nodes.headOption.flatMap(parseRotationForParams) private def parseZoomLevel(nodes: NodeSeq) = @@ -233,7 +239,7 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener } ?~ Messages("nml.node.id.invalid", "branchpoint", getSingleAttribute(branchPoint, "id")) }.toList.toSingleBox(Messages("nml.element.invalid", "branchpoints")) - private def parsePoint3D(node: XMLNode) = { + private def parseVec3Int(node: XMLNode) = { val xText = getSingleAttribute(node, "x") val yText = getSingleAttribute(node, "y") val zText = getSingleAttribute(node, "z") @@ -241,7 +247,7 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener x <- xText.toIntOpt.orElse(xText.toFloatOpt.map(math.round)) y <- yText.toIntOpt.orElse(yText.toFloatOpt.map(math.round)) z <- zText.toIntOpt.orElse(zText.toFloatOpt.map(math.round)) - } yield Point3D(x, y, z) + } yield Vec3Int(x, y, z) } private def parseRotationForParams(node: XMLNode) = @@ -249,14 +255,14 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener rotX <- getSingleAttribute(node, "xRot").toDoubleOpt rotY <- getSingleAttribute(node, "yRot").toDoubleOpt rotZ <- getSingleAttribute(node, "zRot").toDoubleOpt - } yield Vector3D(rotX, rotY, rotZ) + } yield Vec3Double(rotX, rotY, rotZ) private def parseRotationForNode(node: XMLNode) = for { rotX <- getSingleAttribute(node, "rotX").toDoubleOpt rotY <- getSingleAttribute(node, "rotY").toDoubleOpt rotZ <- getSingleAttribute(node, "rotZ").toDoubleOpt - } yield Vector3D(rotX, rotY, rotZ) + } yield Vec3Double(rotX, rotY, rotZ) private def parseColorOpt(node: XMLNode) = for { @@ -265,7 +271,7 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener colorGreen <- getSingleAttribute(node, "color.b").toFloatOpt colorAlpha <- getSingleAttribute(node, "color.a").toFloatOpt } yield { - Color(colorRed, colorBlue, colorGreen, colorAlpha) + ColorProto(colorRed, colorBlue, colorGreen, colorAlpha) } private def parseColor(node: XMLNode) = @@ -277,7 +283,7 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener private def parseGroupId(node: XMLNode) = getSingleAttribute(node, "groupId").toIntOpt - private def parseVisibility(node: XMLNode, color: Option[Color]): Option[Boolean] = + private def parseVisibility(node: XMLNode, color: Option[ColorProto]): Option[Boolean] = getSingleAttribute(node, "isVisible").toBooleanOpt match { case Some(isVisible) => Some(isVisible) case None => color.map(c => !c.a.isNearZero) @@ -382,7 +388,7 @@ object NmlParser extends LazyLogging with ProtoGeometryImplicits with ColorGener for { id <- nodeIdText.toIntOpt ?~ Messages("nml.node.id.invalid", "", nodeIdText) radius = getSingleAttribute(node, "radius").toFloatOpt.getOrElse(NodeDefaults.radius) - position <- parsePoint3D(node) ?~ Messages("nml.node.attribute.invalid", "position", id) + position <- parseVec3Int(node) ?~ Messages("nml.node.attribute.invalid", "position", id) } yield { val viewport = parseViewport(node) val resolution = parseResolution(node) diff --git a/app/models/annotation/nml/NmlResults.scala b/app/models/annotation/nml/NmlResults.scala index 54d8ac59b29..a9f0d454770 100644 --- a/app/models/annotation/nml/NmlResults.scala +++ b/app/models/annotation/nml/NmlResults.scala @@ -5,6 +5,7 @@ import java.io.File import com.scalableminds.webknossos.datastore.SkeletonTracing.SkeletonTracing import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing import com.typesafe.scalalogging.LazyLogging +import models.annotation.UploadedVolumeLayer import net.liftweb.common.{Box, Empty, Failure, Full} import play.api.libs.Files.TemporaryFile @@ -13,8 +14,6 @@ object NmlResults extends LazyLogging { sealed trait NmlParseResult { def fileName: String - def bothTracingOpts: Option[(Option[SkeletonTracing], Option[(VolumeTracing, String)])] = None - def description: Option[String] = None def succeeded: Boolean @@ -33,14 +32,11 @@ object NmlResults extends LazyLogging { case class NmlParseSuccess(fileName: String, skeletonTracing: Option[SkeletonTracing], - volumeTracingWithDataLocation: Option[(VolumeTracing, String)], + volumeLayers: List[UploadedVolumeLayer], _description: String) extends NmlParseResult { def succeeded = true - override def bothTracingOpts: Option[(Option[SkeletonTracing], Option[(VolumeTracing, String)])] = - Some((skeletonTracing, volumeTracingWithDataLocation)) - override def description: Option[String] = Some(_description) override def withName(name: String): NmlParseResult = this.copy(fileName = name) @@ -69,6 +65,7 @@ object NmlResults extends LazyLogging { case _ => false } + // Used in task creation. Can only be used with single-layer volumes def toBoxes: List[TracingBoxContainer] = parseResults.map { parseResult => val successBox = parseResult.toSuccessBox @@ -82,11 +79,14 @@ object NmlResults extends LazyLogging { case _ => Failure("") } val volumeBox = successBox match { - case Full(success) => - success.volumeTracingWithDataLocation match { - case Some((tracing, name)) => Full((tracing, otherFiles.get(name).map(_.path.toFile))) - case None => Empty + case Full(success) if success.volumeLayers.length <= 1 => + success.volumeLayers.headOption match { + case Some(UploadedVolumeLayer(tracing, dataZipLocation, _)) => + Full((tracing, otherFiles.get(dataZipLocation).map(_.path.toFile))) + case None => Empty } + case Full(success) if success.volumeLayers.length > 1 => + Failure("Cannot create tasks from multi-layer volume annotations.") case f: Failure => f case _ => Failure("") } diff --git a/app/models/annotation/nml/NmlVolumeTag.scala b/app/models/annotation/nml/NmlVolumeTag.scala new file mode 100644 index 00000000000..cda53db23ef --- /dev/null +++ b/app/models/annotation/nml/NmlVolumeTag.scala @@ -0,0 +1,3 @@ +package models.annotation.nml + +case class NmlVolumeTag(dataZipPath: String, fallbackLayerName: Option[String], name: Option[String]) {} diff --git a/app/models/annotation/nml/NmlWriter.scala b/app/models/annotation/nml/NmlWriter.scala index e5ed036911c..42343641b04 100644 --- a/app/models/annotation/nml/NmlWriter.scala +++ b/app/models/annotation/nml/NmlWriter.scala @@ -1,6 +1,6 @@ package models.annotation.nml -import com.scalableminds.util.geometry.Scale +import com.scalableminds.util.geometry.Vec3Double import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.util.xml.Xml import com.scalableminds.webknossos.datastore.SkeletonTracing._ @@ -20,14 +20,14 @@ case class NmlParameters( dataSetName: String, organizationName: String, description: Option[String], - scale: Option[Scale], + scale: Option[Vec3Double], createdTimestamp: Long, - editPosition: Point3D, - editRotation: Vector3D, + editPosition: Vec3IntProto, + editRotation: Vec3DoubleProto, zoomLevel: Double, activeNodeId: Option[Int], - userBoundingBoxes: Seq[NamedBoundingBox], - taskBoundingBox: Option[BoundingBox] + userBoundingBoxes: Seq[NamedBoundingBoxProto], + taskBoundingBox: Option[BoundingBoxProto] ) class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits { @@ -35,7 +35,7 @@ class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits { def toNmlStream(annotationLayers: List[FetchedAnnotationLayer], annotation: Option[Annotation], - scale: Option[Scale], + scale: Option[Vec3Double], volumeFilename: Option[String], organizationName: String, annotationOwner: Option[User], @@ -57,7 +57,7 @@ class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits { def toNml(annotationLayers: List[FetchedAnnotationLayer], annotation: Option[Annotation], - scale: Option[Scale], + scale: Option[Vec3Double], volumeFilename: Option[String], organizationName: String, annotationOwner: Option[User], @@ -93,7 +93,7 @@ class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits { volumeLayers: List[FetchedAnnotationLayer], annotation: Option[Annotation], organizationName: String, - scale: Option[Scale]): Fox[NmlParameters] = + scale: Option[Vec3Double]): Fox[NmlParameters] = for { parameterSourceAnnotationLayer <- selectLayerWithPrecedence(skeletonLayers, volumeLayers) nmlParameters = parameterSourceAnnotationLayer.tracing match { @@ -108,7 +108,7 @@ class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits { s.editRotation, s.zoomLevel, s.activeNodeId, - s.userBoundingBoxes ++ s.userBoundingBox.map(NamedBoundingBox(0, None, None, None, _)), + s.userBoundingBoxes ++ s.userBoundingBox.map(NamedBoundingBoxProto(0, None, None, None, _)), s.boundingBox ) case Right(v) => @@ -122,7 +122,7 @@ class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits { v.editRotation, v.zoomLevel, None, - v.userBoundingBoxes ++ v.userBoundingBox.map(NamedBoundingBox(0, None, None, None, _)), + v.userBoundingBoxes ++ v.userBoundingBox.map(NamedBoundingBoxProto(0, None, None, None, _)), if (annotation.exists(_._task.isDefined)) Some(v.boundingBox) else None ) } @@ -310,7 +310,7 @@ class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits { } } - def writeBoundingBox(b: BoundingBox)(implicit writer: XMLStreamWriter): Unit = { + def writeBoundingBox(b: BoundingBoxProto)(implicit writer: XMLStreamWriter): Unit = { writer.writeAttribute("topLeftX", b.topLeft.x.toString) writer.writeAttribute("topLeftY", b.topLeft.y.toString) writer.writeAttribute("topLeftZ", b.topLeft.z.toString) @@ -319,7 +319,7 @@ class NmlWriter @Inject()(implicit ec: ExecutionContext) extends FoxImplicits { writer.writeAttribute("depth", b.depth.toString) } - def writeColor(color: Option[Color])(implicit writer: XMLStreamWriter): Unit = { + def writeColor(color: Option[ColorProto])(implicit writer: XMLStreamWriter): Unit = { writer.writeAttribute("color.r", color.map(_.r.toString).getOrElse("")) writer.writeAttribute("color.g", color.map(_.g.toString).getOrElse("")) writer.writeAttribute("color.b", color.map(_.b.toString).getOrElse("")) diff --git a/app/models/binary/DataSet.scala b/app/models/binary/DataSet.scala index 78b5802e6e9..3bc284a5139 100755 --- a/app/models/binary/DataSet.scala +++ b/app/models/binary/DataSet.scala @@ -1,7 +1,7 @@ package models.binary import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Scale} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} import com.scalableminds.util.tools.{Fox, FoxImplicits, JsonHelper} import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration @@ -40,7 +40,7 @@ case class DataSet( isPublic: Boolean, isUsable: Boolean, name: String, - scale: Option[Scale], + scale: Option[Vec3Double], sharingToken: Option[String], status: String, logoUrl: Option[String], @@ -65,15 +65,15 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, def isDeletedColumn(x: Datasets): Rep[Boolean] = x.isdeleted - private def parseScaleOpt(literalOpt: Option[String]): Fox[Option[Scale]] = literalOpt match { + private def parseScaleOpt(literalOpt: Option[String]): Fox[Option[Vec3Double]] = literalOpt match { case Some(literal) => for { - scale <- Scale.fromList(parseArrayTuple(literal).map(_.toFloat)) ?~> "could not parse edit position" + scale <- Vec3Double.fromList(parseArrayTuple(literal).map(_.toDouble)) ?~> "could not parse dataset scale" } yield Some(scale) case None => Fox.successful(None) } - private def writeScaleLiteral(scale: Scale): String = + private def writeScaleLiteral(scale: Vec3Double): String = writeStructTuple(List(scale.x, scale.y, scale.z).map(_.toString)) def parse(r: DatasetsRow): Fox[DataSet] = @@ -339,12 +339,12 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, class DataSetResolutionsDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) extends SimpleSQLDAO(sqlClient) { - def parseRow(row: DatasetResolutionsRow): Fox[Point3D] = + def parseRow(row: DatasetResolutionsRow): Fox[Vec3Int] = for { - resolution <- Point3D.fromList(parseArrayTuple(row.resolution).map(_.toInt)) ?~> "could not parse resolution" + resolution <- Vec3Int.fromList(parseArrayTuple(row.resolution).map(_.toInt)) ?~> "could not parse resolution" } yield resolution - def findDataResolutionForLayer(dataSetId: ObjectId, dataLayerName: String): Fox[List[Point3D]] = + def findDataResolutionForLayer(dataSetId: ObjectId, dataLayerName: String): Fox[List[Vec3Int]] = for { rows <- run( DatasetResolutions.filter(r => r._Dataset === dataSetId.id && r.datalayername === dataLayerName).result) @@ -388,7 +388,7 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: .fromSQL(parseArrayTuple(row.boundingbox).map(_.toInt)) .toFox ?~> "Could not parse boundingbox" elementClass <- ElementClass.fromString(row.elementclass).toFox ?~> "Could not parse Layer ElementClass" - standinResolutions: Option[List[Point3D]] = if (skipResolutions) Some(List.empty[Point3D]) else None + standinResolutions: Option[List[Vec3Int]] = if (skipResolutions) Some(List.empty[Vec3Int]) else None resolutions <- Fox.fillOption(standinResolutions)( dataSetResolutionsDAO.findDataResolutionForLayer(dataSetId, row.name) ?~> "Could not find resolution for layer") defaultViewConfigurationOpt <- Fox.runOptional(row.defaultviewconfiguration)( diff --git a/app/models/binary/WKRemoteDataStoreClient.scala b/app/models/binary/WKRemoteDataStoreClient.scala index 31330838eb4..f77428ee3ec 100644 --- a/app/models/binary/WKRemoteDataStoreClient.scala +++ b/app/models/binary/WKRemoteDataStoreClient.scala @@ -1,6 +1,6 @@ package models.binary -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.models.ImageThumbnail import com.scalableminds.webknossos.datastore.rpc.RPC @@ -19,7 +19,7 @@ class WKRemoteDataStoreClient(dataStore: DataStore, dataSet: DataSet, rpc: RPC) width: Int, height: Int, zoom: Option[Double], - center: Option[Point3D]): Fox[Array[Byte]] = { + center: Option[Vec3Int]): Fox[Array[Byte]] = { logger.debug(s"Thumbnail called for: $organizationName-${dataSet.name} Layer: $dataLayerName") rpc(s"${dataStore.url}/data/datasets/${urlEncode(organizationName)}/${dataSet.urlEncodedName}/layers/$dataLayerName/thumbnail.json") .addQueryString("token" -> RpcTokenHolder.webKnossosToken) diff --git a/app/models/job/Job.scala b/app/models/job/Job.scala index 37e79b7f07b..9666a8c9fae 100644 --- a/app/models/job/Job.scala +++ b/app/models/job/Job.scala @@ -72,7 +72,7 @@ case class Job( } case "export_tiff" => Some(s"$dataStorePublicUrl/data/exports/${_id.id}/download") - case "infer_nuclei" => + case "infer_nuclei" | "infer_neurons" => returnValue.map { resultDatasetName => s"/datasets/$organizationName/$resultDatasetName/view" } diff --git a/app/models/mesh/Mesh.scala b/app/models/mesh/Mesh.scala index be30dd48504..610443afc93 100644 --- a/app/models/mesh/Mesh.scala +++ b/app/models/mesh/Mesh.scala @@ -2,7 +2,7 @@ package models.mesh import com.google.common.io.BaseEncoding import com.scalableminds.util.accesscontext.DBAccessContext -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.schema.Tables._ import javax.inject.Inject @@ -19,7 +19,7 @@ case class MeshInfo( _id: ObjectId, _annotation: ObjectId, description: String, - position: Point3D, + position: Vec3Int, created: Long = System.currentTimeMillis, isDeleted: Boolean = false ) @@ -27,13 +27,13 @@ case class MeshInfo( case class MeshInfoParameters( annotationId: ObjectId, description: String, - position: Point3D, + position: Vec3Int, ) object MeshInfoParameters { implicit val meshInfoParametersReads: Reads[MeshInfoParameters] = ((__ \ "annotationId").read[String](ObjectId.stringObjectIdReads("teamId")) and (__ \ "description").read[String] and - (__ \ "position").read[Point3D])((annotationId, description, position) => + (__ \ "position").read[Vec3Int])((annotationId, description, position) => MeshInfoParameters(ObjectId(annotationId), description, position)) } @@ -72,7 +72,7 @@ class MeshDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) def parseInfo(r: InfoTuple): Fox[MeshInfo] = for { - position <- Point3D.fromList(parseArrayTuple(r._4).map(_.toInt)) ?~> "could not parse mesh position" + position <- Vec3Int.fromList(parseArrayTuple(r._4).map(_.toInt)) ?~> "could not parse mesh position" } yield { MeshInfo( ObjectId(r._1), //_id @@ -111,7 +111,7 @@ class MeshDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) """) } yield () - def updateOne(id: ObjectId, _annotation: ObjectId, description: String, position: Point3D)( + def updateOne(id: ObjectId, _annotation: ObjectId, description: String, position: Vec3Int)( implicit ctx: DBAccessContext): Fox[Unit] = for { _ <- assertUpdateAccess(id) diff --git a/app/models/task/Task.scala b/app/models/task/Task.scala index 1f5fb03742d..77480c16ce9 100755 --- a/app/models/task/Task.scala +++ b/app/models/task/Task.scala @@ -1,7 +1,7 @@ package models.task import com.scalableminds.util.accesscontext.DBAccessContext -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Vector3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int, Vec3Double} import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.schema.Tables.{profile, _} import javax.inject.Inject @@ -24,8 +24,8 @@ case class Task( openInstances: Long, tracingTime: Option[Long], boundingBox: Option[BoundingBox], - editPosition: Point3D, - editRotation: Vector3D, + editPosition: Vec3Int, + editRotation: Vec3Double, creationInfo: Option[String], created: Long = System.currentTimeMillis(), isDeleted: Boolean = false @@ -40,8 +40,8 @@ class TaskDAO @Inject()(sqlClient: SQLClient, projectDAO: ProjectDAO)(implicit e def parse(r: TasksRow): Fox[Task] = for { - editPosition <- Point3D.fromList(parseArrayTuple(r.editposition).map(_.toInt)) ?~> "could not parse edit position" - editRotation <- Vector3D.fromList(parseArrayTuple(r.editrotation).map(_.toDouble)) ?~> "could not parse edit rotation" + editPosition <- Vec3Int.fromList(parseArrayTuple(r.editposition).map(_.toInt)) ?~> "could not parse edit position" + editRotation <- Vec3Double.fromList(parseArrayTuple(r.editrotation).map(_.toDouble)) ?~> "could not parse edit rotation" } yield { Task( ObjectId(r._Id), diff --git a/app/models/task/TaskCreationParameters.scala b/app/models/task/TaskCreationParameters.scala index 653b9020598..ae073f0f1e0 100644 --- a/app/models/task/TaskCreationParameters.scala +++ b/app/models/task/TaskCreationParameters.scala @@ -1,6 +1,6 @@ package models.task -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Vector3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int, Vec3Double} import models.user.Experience import play.api.libs.json.{Format, Json} @@ -12,8 +12,8 @@ case class TaskParameters( scriptId: Option[String], boundingBox: Option[BoundingBox], dataSet: String, - editPosition: Point3D, - editRotation: Vector3D, + editPosition: Vec3Int, + editRotation: Vec3Double, creationInfo: Option[String], description: Option[String], baseAnnotation: Option[BaseAnnotation] diff --git a/app/models/task/TaskCreationService.scala b/app/models/task/TaskCreationService.scala index 60a3359ac38..04db9d1d8cc 100644 --- a/app/models/task/TaskCreationService.scala +++ b/app/models/task/TaskCreationService.scala @@ -3,7 +3,7 @@ package models.task import java.io.File import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Vector3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int, Vec3Double} import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.SkeletonTracing.{SkeletonTracing, SkeletonTracingOpt, SkeletonTracings} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing @@ -247,7 +247,7 @@ class TaskCreationService @Inject()(taskTypeService: TaskTypeService, volumeTracing: Box[VolumeTracing], fileName: Box[String], description: Box[Option[String]])(implicit m: MessagesProvider): Box[TaskParameters] = { - val paramBox: Box[(Option[BoundingBox], String, Point3D, Vector3D)] = skeletonTracing match { + val paramBox: Box[(Option[BoundingBox], String, Vec3Int, Vec3Double)] = skeletonTracing match { case Full(tracing) => Full((tracing.boundingBox, tracing.dataSetName, tracing.editPosition, tracing.editRotation)) case f: Failure => f case Empty => diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index 4f2154da3b6..244261bf69c 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -327,7 +327,8 @@ class UserService @Inject()(conf: WkConf, "novelUserExperienceInfos" -> novelUserExperienceInfos, "selectedTheme" -> multiUser.selectedTheme, "created" -> user.created, - "lastTaskTypeId" -> user.lastTaskTypeId.map(_.toString) + "lastTaskTypeId" -> user.lastTaskTypeId.map(_.toString), + "isSuperUser" -> multiUser.isSuperUser ) } } diff --git a/app/models/user/time/TimeSpanService.scala b/app/models/user/time/TimeSpanService.scala index 37a0bc03eea..b5203eea23a 100644 --- a/app/models/user/time/TimeSpanService.scala +++ b/app/models/user/time/TimeSpanService.scala @@ -90,7 +90,11 @@ class TimeSpanService @Inject()(annotationDAO: AnnotationDAO, @SuppressWarnings(Array("TraversableHead", "TraversableLast")) // Only functions call this which put at least one timestamp in the seq private def trackTime(timestamps: Seq[Long], _user: ObjectId, _annotation: Annotation)( - implicit ctx: DBAccessContext) = { + implicit ctx: DBAccessContext): Fox[Unit] = { + if (timestamps.isEmpty) { + logger.warn("Timetracking called with empty timestamps list.") + return Fox.successful(()) + } // Only if the annotation belongs to the user, we are going to log the time on the annotation val annotation = if (_annotation._user == _user) Some(_annotation) else None val start = timestamps.head diff --git a/app/views/main.scala.html b/app/views/main.scala.html index ce689052473..5d9fdbff282 100755 --- a/app/views/main.scala.html +++ b/app/views/main.scala.html @@ -73,24 +73,12 @@
@if(conf.GoogleAnalytics.trackingId.nonEmpty) { + } diff --git a/clean b/clean index b74f199057e..11e9c6602be 100755 --- a/clean +++ b/clean @@ -12,6 +12,8 @@ rm -rf webknossos-tracingstore/target rm -rf webknossos-tracingstore/project/target rm -rf webknossos-tracingstore/project/project/target rm -rf node_modules +rm -rf .eslintcache mkdir target echo "[info] Done. Don’t forget to run yarn install before the next yarn start." + diff --git a/conf/messages b/conf/messages index 1e18f301a55..34f5d7aec82 100644 --- a/conf/messages +++ b/conf/messages @@ -340,6 +340,7 @@ job.couldNotRunCubing = Failed to start WKW conversion job. job.couldNotRunTiffExport = Failed to start Tiff export job. job.couldNotRunComputeMeshFile = Failed to start mesh file computation job. job.couldNotRunNucleiInferral = Failed to start nuclei inferral job. +job.couldNotRunNeuronInferral = Failed to start neuron inferral job. job.couldNotRunGlobalizeFloodfills = Failed to start job for globalizing floodfills. job.disabled = Long-running jobs are not enabled for this webKnossos instance. jobs.worker.notFound = Could not find this worker in the database. @@ -349,6 +350,7 @@ job.export.tiff.volumeExceeded = The volume of the selected bounding box is too job.export.tiff.edgeLengthExceeded = An edge length of the selected bounding box is too large. job.export.notAllowed.organization = Currently tiff export is only allowed for datasets of your own organization. job.inferNuclei.notAllowed.organization = Currently nuclei inferral is only allowed for datasets of your own organization. +job.inferNeurons.notAllowed.organization = Currently neuron inferral is only allowed for datasets of your own organization. job.meshFile.notAllowed.organization = Calculating mesh files is only allowed for datasets of your own organization. job.globalizeFloodfill.notAllowed.organization = Globalizing floodfills is only allowed for datasets of your own organization. diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index 747dc419ae1..d285c61cfcb 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -207,6 +207,7 @@ POST /jobs/run/convertToWkw/:organizationName/:dataSetName c POST /jobs/run/computeMeshFile/:organizationName/:dataSetName controllers.JobsController.runComputeMeshFileJob(organizationName: String, dataSetName: String, layerName: String, mag: String, agglomerateView: Option[String]) POST /jobs/run/exportTiff/:organizationName/:dataSetName controllers.JobsController.runExportTiffJob(organizationName: String, dataSetName: String, bbox: String, layerName: Option[String], tracingId: Option[String], tracingVersion: Option[String], annotationId: Option[String], annotationType: Option[String], hideUnmappedIds: Option[Boolean], mappingName: Option[String], mappingType: Option[String]) POST /jobs/run/inferNuclei/:organizationName/:dataSetName controllers.JobsController.runInferNucleiJob(organizationName: String, dataSetName: String, layerName: Option[String]) +POST /jobs/run/inferNeurons/:organizationName/:dataSetName controllers.JobsController.runInferNeuronsJob(organizationName: String, dataSetName: String, layerName: String, bbox: String) POST /jobs/run/globalizeFloodfills/:organizationName/:dataSetName controllers.JobsController.runGlobalizeFloodfills(organizationName: String, dataSetName: String, newDataSetName: Option[String], layerName: Option[String], annotationId: Option[String], annotationType: Option[String]) GET /jobs/:id controllers.JobsController.get(id: String) PATCH /jobs/:id/cancel controllers.JobsController.cancel(id: String) diff --git a/flow-typed/npm/puppeteer_v1.2.x.js b/flow-typed/npm/puppeteer_v1.2.x.js index c6866aaef6d..102f41d1a7c 100644 --- a/flow-typed/npm/puppeteer_v1.2.x.js +++ b/flow-typed/npm/puppeteer_v1.2.x.js @@ -5,8 +5,8 @@ * https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/puppeteer */ -declare module 'puppeteer' { - import type { ChildProcess } from 'child_process'; +declare module "puppeteer" { + import type { ChildProcess } from "child_process"; declare class OverridableEventEmitter extends events$EventEmitter { /* eslint-disable flowtype/no-weak-types */ @@ -129,7 +129,7 @@ declare module 'puppeteer' { message(): string, /** The dialog type. Dialog's type, can be one of `alert`, `beforeunload`, `confirm` or `prompt`. */ - type(): 'alert' | 'beforeunload' | 'confirm' | 'prompt', + type(): "alert" | "beforeunload" | "confirm" | "prompt", }; /** ConsoleMessage objects are dispatched by page via the 'console' event. */ @@ -141,52 +141,52 @@ declare module 'puppeteer' { text(): string, type(): - | 'log' - | 'debug' - | 'info' - | 'error' - | 'warning' - | 'dir' - | 'dirxml' - | 'table' - | 'trace' - | 'clear' - | 'startGroup' - | 'startGroupCollapsed' - | 'endGroup' - | 'assert' - | 'profile' - | 'profileEnd' - | 'count' - | 'timeEnd', + | "log" + | "debug" + | "info" + | "error" + | "warning" + | "dir" + | "dirxml" + | "table" + | "trace" + | "clear" + | "startGroup" + | "startGroupCollapsed" + | "endGroup" + | "assert" + | "profile" + | "profileEnd" + | "count" + | "timeEnd", }; declare type PageEvents = - | 'console' - | 'dialog' - | 'error' - | 'frameattached' - | 'framedetached' - | 'framenavigated' - | 'load' - | 'pageerror' - | 'request' - | 'requestfailed' - | 'requestfinished' - | 'response'; + | "console" + | "dialog" + | "error" + | "frameattached" + | "framedetached" + | "framenavigated" + | "load" + | "pageerror" + | "request" + | "requestfailed" + | "requestfinished" + | "response"; declare type BrowserEvents = - | 'disconnected' - | 'targetchanged' - | 'targetcreated' - | 'targetdestroyed'; + | "disconnected" + | "targetchanged" + | "targetcreated" + | "targetdestroyed"; declare type AuthOptions = { password: string, username: string, }; - declare type MouseButtons = 'left' | 'right' | 'middle'; + declare type MouseButtons = "left" | "right" | "middle"; declare type ClickOptions = { /** defaults to left */ @@ -220,7 +220,7 @@ declare module 'puppeteer' { path: string, /** The cookie same site definition. */ - sameSite: 'Strict' | 'Lax', + sameSite: "Strict" | "Lax", /** The cookie secure flag. */ secure: boolean, @@ -255,7 +255,7 @@ declare module 'puppeteer' { path?: string, /** The cookie same site definition. */ - sameSite?: 'Strict' | 'Lax', + sameSite?: "Strict" | "Lax", /** The cookie secure flag. */ secure?: boolean, @@ -310,7 +310,7 @@ declare module 'puppeteer' { declare type EvaluateFn = string | ((...args: Array) => mixed); - declare type LoadEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; + declare type LoadEvent = "load" | "domcontentloaded" | "networkidle0" | "networkidle2"; /** The navigation options. */ declare type NavigationOptions = { @@ -327,16 +327,16 @@ declare module 'puppeteer' { }; declare type PDFFormat = - | 'Letter' - | 'Legal' - | 'Tabload' - | 'Ledger' - | 'A0' - | 'A1' - | 'A2' - | 'A3' - | 'A4' - | 'A5'; + | "Letter" + | "Legal" + | "Tabload" + | "Ledger" + | "A0" + | "A1" + | "A2" + | "A3" + | "A4" + | "A5"; declare type PDFOptions = { /** @@ -454,7 +454,7 @@ declare module 'puppeteer' { * The screenshot type. * @default png */ - type?: 'jpeg' | 'png', + type?: "jpeg" | "png", }; /** Options for `addStyleTag` */ @@ -484,7 +484,7 @@ declare module 'puppeteer' { }; declare type PageFnOptions = { - polling?: 'raf' | 'mutation' | number, + polling?: "raf" | "mutation" | number, timeout?: number, }; @@ -675,22 +675,22 @@ declare module 'puppeteer' { }; declare type Headers = { [key: string]: string }; - declare type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS'; + declare type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE" | "OPTIONS"; declare type ResourceType = - | 'document' - | 'stylesheet' - | 'image' - | 'media' - | 'font' - | 'script' - | 'texttrack' - | 'xhr' - | 'fetch' - | 'eventsource' - | 'websocket' - | 'manifest' - | 'other'; + | "document" + | "stylesheet" + | "image" + | "media" + | "font" + | "script" + | "texttrack" + | "xhr" + | "fetch" + | "eventsource" + | "websocket" + | "manifest" + | "other"; declare type Overrides = { headers?: Headers, @@ -838,10 +838,7 @@ declare module 'puppeteer' { */ $$eval: ( selector: string, - pageFunction: ( - elements: NodeList, - ...args: Array - ) => mixed, + pageFunction: (elements: NodeList, ...args: Array) => mixed, ...args: Array ) => Promise; @@ -891,7 +888,7 @@ declare module 'puppeteer' { /** Returns frame's url. */ +url: () => string; - waitFor: ( + waitForTimeout: ( // fn can be an abritary function // eslint-disable-next-line flowtype/no-weak-types selectorOrFunctionOrTimeout: string | number | Function, @@ -909,7 +906,7 @@ declare module 'puppeteer' { waitForSelector: ( selector: string, - options?: {hidden?: boolean, timeout?: number, visible?: boolean} + options?: { hidden?: boolean, timeout?: number, visible?: boolean }, ) => Promise; } @@ -1023,7 +1020,7 @@ declare module 'puppeteer' { emulate(options: $Shape): Promise; /** Emulates the media. */ - emulateMedia(mediaType: 'screen' | 'print' | null): Promise; + emulateMedia(mediaType: "screen" | "print" | null): Promise; /** * Evaluates a function in the page context. @@ -1052,7 +1049,7 @@ declare module 'puppeteer' { */ exposeFunction( name: string, - puppeteerFunction: (...args: Array) => mixed + puppeteerFunction: (...args: Array) => mixed, ): Promise; /** This method fetches an element with selector and focuses it. */ @@ -1109,7 +1106,7 @@ declare module 'puppeteer' { */ on( eventName: K, - handler: (e: $ElementType, ...args: Array) => void + handler: (e: $ElementType, ...args: Array) => void, ): Page; /** @@ -1120,7 +1117,7 @@ declare module 'puppeteer' { */ once( eventName: K, - handler: (e: $ElementType, ...args: Array) => void + handler: (e: $ElementType, ...args: Array) => void, ): Page; /** @@ -1284,7 +1281,7 @@ declare module 'puppeteer' { */ on( eventName: K, - handler: (e: $ElementType, ...args: Array) => void + handler: (e: $ElementType, ...args: Array) => void, ): Browser; /** @@ -1295,7 +1292,7 @@ declare module 'puppeteer' { */ once( eventName: K, - handler: (e: $ElementType, ...args: Array) => void + handler: (e: $ElementType, ...args: Array) => void, ): Browser; /** Promise which resolves to an array of all open pages. */ @@ -1342,7 +1339,7 @@ declare module 'puppeteer' { page(): Promise, /** Identifies what kind of target this is. */ - type(): 'page' | 'service_worker' | 'other', + type(): "page" | "service_worker" | "other", /** Returns the target URL. */ url(): string, diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index 1ad4ebdcfb5..1f47f18318f 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -1053,6 +1053,19 @@ export function startNucleiInferralJob( ); } +export function startNeuronInferralJob( + organizationName: string, + datasetName: string, + layerName: string, + bbox: Vector6, +): Promise { + return Request.receiveJSON( + `/api/jobs/run/inferNeurons/${organizationName}/${datasetName}?layerName=${layerName}&bbox=${bbox.join( + ",", + )}`, + ); +} + export function startGlobalizeFloodfillsJob( organizationName: string, datasetName: string, @@ -1760,7 +1773,7 @@ type IsosurfaceRequest = { position: Vector3, zoomStep: number, segmentId: number, - voxelDimensions: Vector3, + subsamplingStrides: Vector3, cubeSize: Vector3, scale: Vector3, }; @@ -1770,7 +1783,7 @@ export function computeIsosurface( mappingInfo: ActiveMappingInfo, isosurfaceRequest: IsosurfaceRequest, ): Promise<{ buffer: ArrayBuffer, neighbors: Array }> { - const { position, zoomStep, segmentId, voxelDimensions, cubeSize, scale } = isosurfaceRequest; + const { position, zoomStep, segmentId, subsamplingStrides, cubeSize, scale } = isosurfaceRequest; const mapping = mappingInfo.mappingStatus !== MappingStatusEnum.DISABLED ? mappingInfo.mappingName : undefined; const mappingType = @@ -1783,8 +1796,8 @@ export function computeIsosurface( // The back-end needs a small padding at the border of the // bounding box to calculate the mesh. This padding // is added here to the position and bbox size. - position: V3.toArray(V3.sub(position, voxelDimensions)), - cubeSize: V3.toArray(V3.add(cubeSize, voxelDimensions)), + position: V3.toArray(V3.sub(position, subsamplingStrides)), + cubeSize: V3.toArray(V3.add(cubeSize, subsamplingStrides)), zoomStep, // Segment to build mesh for segmentId, @@ -1792,7 +1805,7 @@ export function computeIsosurface( mapping, mappingType, // "size" of each voxel (i.e., only every nth voxel is considered in each dimension) - voxelDimensions, + subsamplingStrides, scale, }, }, diff --git a/frontend/javascripts/admin/job/job_list_view.js b/frontend/javascripts/admin/job/job_list_view.js index af5018c224f..6328bf6263a 100644 --- a/frontend/javascripts/admin/job/job_list_view.js +++ b/frontend/javascripts/admin/job/job_list_view.js @@ -219,7 +219,7 @@ class JobListView extends React.PureComponent { )} ); - } else if (job.type === "infer_nuclei") { + } else if (job.type === "infer_nuclei" || job.type === "infer_neurons") { return ( {job.resultLink && ( diff --git a/frontend/javascripts/libs/format_utils.js b/frontend/javascripts/libs/format_utils.js index 9647f7ccfb3..307a458ef9e 100644 --- a/frontend/javascripts/libs/format_utils.js +++ b/frontend/javascripts/libs/format_utils.js @@ -59,14 +59,37 @@ export function formatScale(scaleArr: ?Vector3, roundTo?: number = 2): string { } } +const nmFactorToUnit = new Map([ + [1e-3, "pm"], + [1, "nm"], + [1e3, "µm"], + [1e6, "mm"], + [1e9, "m"], + [1e12, "km"], +]); +const sortedNmFactors = Array.from(nmFactorToUnit.keys()).sort((a, b) => a - b); + export function formatNumberToLength(lengthInNm: number): string { - if (lengthInNm < 1000) { - return `${lengthInNm.toFixed(0)}${ThinSpace}nm`; - } else if (lengthInNm < 1000000) { - return `${(lengthInNm / 1000).toFixed(1)}${ThinSpace}µm`; - } else { - return `${(lengthInNm / 1000000).toFixed(1)}${ThinSpace}mm`; + const closestFactor = findClosestLengthUnitFactor(lengthInNm); + const unit = nmFactorToUnit.get(closestFactor); + if (unit == null) { + throw new Error("Couldn't look up appropriate length unit."); + } + const lengthInUnit = lengthInNm / closestFactor; + if (lengthInUnit !== Math.floor(lengthInUnit)) { + return `${lengthInUnit.toFixed(1)}${ThinSpace}${unit}`; + } + return `${lengthInUnit}${ThinSpace}${unit}`; +} + +export function findClosestLengthUnitFactor(lengthInNm: number): number { + let closestFactor = sortedNmFactors[0]; + for (const factor of sortedNmFactors) { + if (lengthInNm >= factor) { + closestFactor = factor; + } } + return closestFactor; } export function formatLengthAsVx(lengthInVx: number, roundTo?: number = 2): string { diff --git a/frontend/javascripts/libs/mjs.js b/frontend/javascripts/libs/mjs.js index 07ecab0782a..11694b15d45 100644 --- a/frontend/javascripts/libs/mjs.js +++ b/frontend/javascripts/libs/mjs.js @@ -209,6 +209,10 @@ V3.divide3 = function divide3(a, k, r) { return r; }; +V3.fromMag1ToMag = (vec, targetMag) => V3.floor(V3.divide3(vec, targetMag)); + +V3.fromMagToMag1 = (vec, sourceMag) => V3.floor(V3.scale3(vec, sourceMag)); + const _tmpVec = [0, 0, 0]; V3.scaledSquaredDist = function squaredDist(a, b, scale) { // Computes the distance between two vectors while respecting a 3 dimensional scale @@ -234,4 +238,20 @@ V2.scale2 = function scale2(a, k, r) { return r; }; +// Component-wise minimum of two vectors. +V3.min = (vec1, vec2) => [ + Math.min(vec1[0], vec2[0]), + Math.min(vec1[1], vec2[1]), + Math.min(vec1[2], vec2[2]), +]; + +// Component-wise maximum of two vectors. +V3.max = (vec1, vec2) => [ + Math.max(vec1[0], vec2[0]), + Math.max(vec1[1], vec2[1]), + Math.max(vec1[2], vec2[2]), +]; + +V3.equals = (vec1, vec2) => vec1[0] === vec2[0] && vec1[1] === vec2[1] && vec1[2] === vec2[2]; + export { M4x4, V2, V3 }; diff --git a/frontend/javascripts/libs/progress_callback.js b/frontend/javascripts/libs/progress_callback.js index 9e4ce49d7aa..aaa19285d32 100644 --- a/frontend/javascripts/libs/progress_callback.js +++ b/frontend/javascripts/libs/progress_callback.js @@ -32,6 +32,7 @@ export default function createProgressCallback(options: Options): ProgressCallba isDone: boolean, status: string | React$Node, overridingOptions: $Shape = {}, + finalFeedbackMethod: "success" | "error" | "info" | "warning" = "success", ): Promise<{ hideFn: HideFn }> => { if (hideFn != null) { // Clear old progress message @@ -50,10 +51,10 @@ export default function createProgressCallback(options: Options): ProgressCallba const pauseDelay = overridingOptions.pauseDelay || options.pauseDelay; await sleep(pauseDelay); } else { - // Show success message and clear that after + // Show (success) message and clear that after // ${successDelay} ms const successDelay = overridingOptions.successMessageDelay || options.successMessageDelay; - hideFn = message.success({ + hideFn = message[finalFeedbackMethod]({ content: status, duration: 0, key: overridingOptions.key || options.key, diff --git a/frontend/javascripts/libs/utils.js b/frontend/javascripts/libs/utils.js index eed23375df5..9683f7b6ade 100644 --- a/frontend/javascripts/libs/utils.js +++ b/frontend/javascripts/libs/utils.js @@ -163,6 +163,13 @@ export function capitalize(str: string): string { return str[0].toUpperCase() + str.slice(1); } +export function capitalizeWords(str: string): string { + return str + .split(" ") + .map(capitalize) + .join(" "); +} + function intToHex(int: number, digits: number = 6): string { return (_.repeat("0", digits) + int.toString(16)).slice(-digits); } diff --git a/frontend/javascripts/oxalis/api/api_latest.js b/frontend/javascripts/oxalis/api/api_latest.js index 7909d7043c8..0cd912899c7 100644 --- a/frontend/javascripts/oxalis/api/api_latest.js +++ b/frontend/javascripts/oxalis/api/api_latest.js @@ -1233,16 +1233,16 @@ class DataApi { _zoomStep: ?number = null, ) { const layer = getLayerByName(Store.getState().dataset, layerName); + const resolutionInfo = getResolutionInfo(layer.resolutions); let zoomStep; if (_zoomStep != null) { zoomStep = _zoomStep; } else { - const resolutionInfo = getResolutionInfo(layer.resolutions); zoomStep = resolutionInfo.getClosestExistingIndex(0); } - const { resolutions } = layer; + const resolutions = resolutionInfo.getDenseResolutions(); const bucketAddresses = this.getBucketAddressesInCuboid(bbox, resolutions, zoomStep); if (bucketAddresses.length > 15000) { console.warn( diff --git a/frontend/javascripts/oxalis/constants.js b/frontend/javascripts/oxalis/constants.js index 69b63ce2459..38c984d7f43 100644 --- a/frontend/javascripts/oxalis/constants.js +++ b/frontend/javascripts/oxalis/constants.js @@ -1,7 +1,4 @@ -/** - * constants.js - * @flow - */ +// @flow export const ViewModeValues = ["orthogonal", "flight", "oblique", "volume"]; // MODE_PLANE_TRACING | MODE_ARBITRARY | MODE_ARBITRARY_PLANE | MODE_VOLUME export const ViewModeValuesIndices = { Orthogonal: 0, Flight: 1, Oblique: 2, Volume: 3 }; diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 83059c9a52d..d5106a88ffc 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -136,6 +136,22 @@ class SceneController { return cube; }; + window.addVoxelMesh = (position: Vector3, _cubeLength: Vector3, optColor?: string) => { + // Shrink voxels a bit so that it's easier to identify individual voxels. + const cubeLength = _cubeLength.map(el => el * 0.9); + const boxGeometry = new THREE.BoxGeometry(...cubeLength); + const material = new THREE.MeshBasicMaterial({ + color: optColor || 0xff00ff, + opacity: 0.5, + }); + const cube = new THREE.Mesh(boxGeometry, material); + cube.position.x = position[0] + cubeLength[0] / 2; + cube.position.y = position[1] + cubeLength[1] / 2; + cube.position.z = position[2] + cubeLength[2] / 2; + this.rootNode.add(cube); + return cube; + }; + let renderedLines = []; window.addLine = (a: Vector3, b: Vector3) => { diff --git a/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js b/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js index b8337336a57..a08391b70e2 100644 --- a/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js @@ -80,11 +80,15 @@ export class ResolutionInfo { } getResolutionsWithIndices(): Array<[number, Vector3]> { - return Array.from(this.resolutionMap.entries()).map(entry => { - const [powerOfTwo, resolution] = entry; - const resolutionIndex = Math.log2(powerOfTwo); - return [resolutionIndex, resolution]; - }); + return _.sortBy( + Array.from(this.resolutionMap.entries()).map(entry => { + const [powerOfTwo, resolution] = entry; + const resolutionIndex = Math.log2(powerOfTwo); + return [resolutionIndex, resolution]; + }), + // Sort by resolutionIndex + tuple => tuple[0], + ); } indexToPowerOf2(index: number): number { diff --git a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js index ec2e06117c1..6e5d9c5c4f5 100644 --- a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js @@ -5,6 +5,7 @@ import type { ReadOnlyTracing, SkeletonTracing, Tracing, + UserBoundingBox, VolumeTracing, } from "oxalis/store"; import { type ServerTracing, type TracingType, TracingTypeEnum } from "types/api_flow_types"; @@ -69,3 +70,8 @@ export function selectTracing( return volumeTracing; } + +export const getUserBoundingBoxesFromState = (state: OxalisState): Array => { + const maybeSomeTracing = maybeGetSomeTracing(state.tracing); + return maybeSomeTracing != null ? maybeSomeTracing.userBoundingBoxes : []; +}; diff --git a/frontend/javascripts/oxalis/model/actions/settings_actions.js b/frontend/javascripts/oxalis/model/actions/settings_actions.js index 3e87064102e..3bf137fa1d9 100644 --- a/frontend/javascripts/oxalis/model/actions/settings_actions.js +++ b/frontend/javascripts/oxalis/model/actions/settings_actions.js @@ -45,6 +45,11 @@ type SetHistogramDataAction = { type: "SET_HISTOGRAM_DATA", histogramData: HistogramDataForAllLayers, }; +export type ClipHistogramAction = { + type: "CLIP_HISTOGRAM", + layerName: string, + shouldAdjustClipRange: boolean, +}; type SetFlightmodeRecordingAction = { type: "SET_FLIGHTMODE_RECORDING", value: boolean }; type SetControlModeAction = { type: "SET_CONTROL_MODE", controlMode: ControlMode }; type InitializeGpuSetupAction = { @@ -158,6 +163,15 @@ export const setHistogramDataAction = ( histogramData, }); +export const clipHistogramAction = ( + layerName: string, + shouldAdjustClipRange: boolean, +): ClipHistogramAction => ({ + type: "CLIP_HISTOGRAM", + layerName, + shouldAdjustClipRange, +}); + export const setFlightmodeRecordingAction = (value: boolean): SetFlightmodeRecordingAction => ({ type: "SET_FLIGHTMODE_RECORDING", value, diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index a5a4c548b5c..a1efd78deb2 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -23,6 +23,13 @@ type FloodFillAction = { planeId: OrthoView, callback?: () => void, }; + +export type PerformMinCutAction = { + type: "PERFORM_MIN_CUT", + treeId: number, + boundingBoxId?: number, +}; + type FinishEditingAction = { type: "FINISH_EDITING" }; export type SetActiveCellAction = { type: "SET_ACTIVE_CELL", @@ -85,6 +92,7 @@ export type VolumeTracingAction = | StartEditingAction | AddToLayerAction | FloodFillAction + | PerformMinCutAction | FinishEditingAction | SetActiveCellAction | ClickSegmentAction @@ -146,6 +154,15 @@ export const floodFillAction = ( callback, }); +export const performMinCutAction = ( + treeId: number, + boundingBoxId?: number, +): PerformMinCutAction => ({ + type: "PERFORM_MIN_CUT", + treeId, + boundingBoxId, +}); + export const finishEditingAction = (): FinishEditingAction => ({ type: "FINISH_EDITING", }); diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.js index f2b20e4ebff..c1906bea709 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bounding_box.js @@ -60,6 +60,12 @@ class BoundingBox { return min[0] <= x && x < max[0] && min[1] <= y && y < max[1] && min[2] <= z && z < max[2]; } + containsPoint(vec3: Vector3) { + const [x, y, z] = vec3; + const { min, max } = this; + return min[0] <= x && x < max[0] && min[1] <= y && y < max[1] && min[2] <= z && z < max[2]; + } + containsFullBucket([x, y, z, zoomStep]: Vector4): boolean { const { min, max } = this.getBoxForZoomStep(zoomStep); @@ -83,8 +89,18 @@ class BoundingBox { return new BoundingBox({ min: newMin, max: newMax }); } - chunkIntoBuckets() { + getSize(): Vector3 { const size = V3.sub(this.max, this.min); + return size; + } + + getVolume(): number { + const size = this.getSize(); + return size[0] * size[1] * size[2]; + } + + chunkIntoBuckets() { + const size = this.getSize(); const start = [...this.min]; const chunkSize = [32, 32, 32]; const chunkBorderAlignments = [32, 32, 32]; @@ -118,6 +134,20 @@ class BoundingBox { return boxes; } + + fromMag1ToMag(mag: Vector3): BoundingBox { + const min = [ + Math.floor(this.min[0] / mag[0]), + Math.floor(this.min[1] / mag[1]), + Math.floor(this.min[2] / mag[2]), + ]; + const max = [ + Math.ceil(this.max[0] / mag[0]), + Math.ceil(this.max[1] / mag[1]), + Math.ceil(this.max[2] / mag[2]), + ]; + return new BoundingBox({ min, max }); + } } export default BoundingBox; diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index b4a7d5b1bb0..80a04376251 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -706,12 +706,21 @@ export class DataBucket { }); this.cube.pullQueue.pull(); needsToAwaitBucket = true; + } else if (this.isMissing()) { + // Awaiting is not necessary. } if (needsToAwaitBucket) { await new Promise(resolve => { this.once("bucketLoaded", resolve); + this.once("bucketMissing", resolve); }); } // Bucket has been loaded by now or was loaded already + if (this.isMissing()) { + // In the past, ensureLoaded() never returned if the bucket + // was MISSING. This log might help to discover potential + // bugs which could arise in combination with MISSING buckets. + console.warn("Awaited missing bucket."); + } } } diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js index 004bb9cc136..b5c15f0f03e 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js @@ -27,7 +27,7 @@ import { globalPositionToBucketPosition } from "oxalis/model/helpers/position_co import { listenToStoreProperty } from "oxalis/model/helpers/listener_helpers"; import ArbitraryCubeAdapter from "oxalis/model/bucket_data_handling/arbitrary_cube_adapter"; import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box"; -import PullQueue, { PullQueueConstants } from "oxalis/model/bucket_data_handling/pullqueue"; +import PullQueue from "oxalis/model/bucket_data_handling/pullqueue"; import PushQueue from "oxalis/model/bucket_data_handling/pushqueue"; import Store, { type Mapping } from "oxalis/store"; import TemporalBucketManager from "oxalis/model/bucket_data_handling/temporal_bucket_manager"; @@ -772,27 +772,10 @@ class DataCube { async getLoadedBucket(bucketAddress: Vector4) { const bucket = this.getOrCreateBucket(bucketAddress); - if (bucket.type === "null") { - return bucket; - } - - let needsToAwaitBucket = false; - if (bucket.isRequested()) { - needsToAwaitBucket = true; - } else if (bucket.needsRequest()) { - this.pullQueue.add({ - bucket: bucketAddress, - priority: PullQueueConstants.PRIORITY_HIGHEST, - }); - this.pullQueue.pull(); - needsToAwaitBucket = true; - } - if (needsToAwaitBucket) { - await new Promise(resolve => { - bucket.on("bucketLoaded", resolve); - }); - } - // Bucket has been loaded by now or was loaded already + if (bucket.type !== "null") { + await bucket.ensureLoaded(); + } + return bucket; } } diff --git a/frontend/javascripts/oxalis/model/helpers/analytics.js b/frontend/javascripts/oxalis/model/helpers/analytics.js index 265895142e6..a3a7fd7791f 100644 --- a/frontend/javascripts/oxalis/model/helpers/analytics.js +++ b/frontend/javascripts/oxalis/model/helpers/analytics.js @@ -8,23 +8,21 @@ function getOrganization() { return activeUser != null ? activeUser.organization : null; } -function gaGuard(...params) { - if (typeof window.ga !== "undefined" && window.ga !== null) { - window.ga(...params); +function gtagGuard(...params) { + if (typeof window.gtag !== "undefined" && window.gtag !== null) { + window.gtag(...params); } } // The void return type is needed for flow to check successfully export function trackAction(action: string): void { - gaGuard("send", "event", "Action", action, getOrganization()); + gtagGuard("event", "action", action, getOrganization()); } export function trackVersion(version: string): void { - gaGuard("send", { - hitType: "event", - eventCategory: "pageLoad", - eventAction: "version", - eventLabel: version, + gtagGuard("event", "version", { + event_category: "page_load", + event_label: version, }); } @@ -37,7 +35,10 @@ export function googleAnalyticsLogClicks(evt: MouseEvent) { // Restrict the textContent to a maximum length const textContent = target.textContent.trim().slice(0, 50); if (textContent.length > 0) { - gaGuard("send", "event", "Click", textContent, getOrganization()); + gtagGuard("event", "click", { + event_category: textContent, + event_label: getOrganization(), + }); } } } diff --git a/frontend/javascripts/oxalis/model/reducers/ui_reducer.js b/frontend/javascripts/oxalis/model/reducers/ui_reducer.js index d18e67703bc..5d20dbd704c 100644 --- a/frontend/javascripts/oxalis/model/reducers/ui_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/ui_reducer.js @@ -65,6 +65,11 @@ function UiReducer(state: OxalisState, action: Action): OxalisState { } case "SET_BUSY_BLOCKING_INFO_ACTION": { + if (action.value.isBusy && state.uiInformation.busyBlockingInfo.isBusy) { + throw new Error( + "Busy-mutex violated. Cannot set isBusy to true, as it is already set to true.", + ); + } return updateKey(state, "uiInformation", { busyBlockingInfo: action.value }); } diff --git a/frontend/javascripts/oxalis/model/sagas/clip_histogram_saga.js b/frontend/javascripts/oxalis/model/sagas/clip_histogram_saga.js new file mode 100644 index 00000000000..f8476954127 --- /dev/null +++ b/frontend/javascripts/oxalis/model/sagas/clip_histogram_saga.js @@ -0,0 +1,102 @@ +// @flow +import Store from "oxalis/store"; +import { type Saga, _takeEvery } from "oxalis/model/sagas/effect-generators"; +import { + updateLayerSettingAction, + type ClipHistogramAction, +} from "oxalis/model/actions/settings_actions"; +import Toast from "libs/toast"; + +import { OrthoViews } from "oxalis/constants"; +import { getConstructorForElementClass } from "oxalis/model/bucket_data_handling/bucket"; +import { getLayerByName } from "oxalis/model/accessors/dataset_accessor"; +import api from "oxalis/api/internal_api"; + +function onThresholdChange(layerName: string, [firstVal, secVal]: [number, number]) { + if (firstVal < secVal) { + Store.dispatch(updateLayerSettingAction(layerName, "intensityRange", [firstVal, secVal])); + } else { + Store.dispatch(updateLayerSettingAction(layerName, "intensityRange", [firstVal, secVal])); + } +} + +async function getClippingValues(layerName: string, thresholdRatio: number = 0.05) { + const { elementClass } = getLayerByName(Store.getState().dataset, layerName); + const [TypedArrayClass] = getConstructorForElementClass(elementClass); + + const [cuboidXY, cuboidXZ, cuboidYZ] = await Promise.all([ + api.data.getViewportData(OrthoViews.PLANE_XY, layerName), + api.data.getViewportData(OrthoViews.PLANE_XZ, layerName), + api.data.getViewportData(OrthoViews.PLANE_YZ, layerName), + ]); + const dataForAllViewPorts = new TypedArrayClass( + cuboidXY.length + cuboidXZ.length + cuboidYZ.length, + ); + + dataForAllViewPorts.set(cuboidXY); + dataForAllViewPorts.set(cuboidXZ, cuboidXY.length); + dataForAllViewPorts.set(cuboidYZ, cuboidXY.length + cuboidXZ.length); + + const localHist = new Map(); + for (let i = 0; i < dataForAllViewPorts.length; i++) { + if (dataForAllViewPorts[i] !== 0) { + const value = localHist.get(dataForAllViewPorts[i]); + localHist.set(dataForAllViewPorts[i], value != null ? value + 1 : 1); + } + } + + const sortedHistKeys = Array.from(localHist.keys()).sort((a, b) => a - b); + const accumulator = new Map(); + let area = 0; + for (const key of sortedHistKeys) { + const value = localHist.get(key); + area += value != null ? value : 0; + accumulator.set(key, area); + } + const thresholdValue = (thresholdRatio * area) / 2.0; + + let lowerClip = -1; + for (const key of sortedHistKeys) { + const value = accumulator.get(key); + if (value != null && value >= thresholdValue) { + lowerClip = key; + break; + } + } + let upperClip = -1; + for (const key of sortedHistKeys.reverse()) { + const value = accumulator.get(key); + if (value != null && value < area - thresholdValue) { + upperClip = key; + break; + } + } + // largest brightness value is first after the keys were reversed + const wiggleRoom = Math.floor(thresholdRatio * sortedHistKeys[0]); + return [lowerClip, upperClip, wiggleRoom]; +} + +async function clipHistogram(layerName: string, shouldAdjustClipRange: boolean) { + const [lowerClip, upperClip, wiggleRoom] = await getClippingValues(layerName); + if (lowerClip === -1 || upperClip === -1) { + Toast.warning( + "The histogram could not be clipped, because the data did not contain any brightness values greater than 0.", + ); + return; + } + if (!shouldAdjustClipRange) { + onThresholdChange(layerName, [lowerClip, upperClip]); + } else { + onThresholdChange(layerName, [lowerClip, upperClip]); + Store.dispatch(updateLayerSettingAction(layerName, "min", lowerClip - wiggleRoom)); + Store.dispatch(updateLayerSettingAction(layerName, "max", upperClip + wiggleRoom)); + } +} + +export function handleClipHistogram(action: ClipHistogramAction): void { + clipHistogram(action.layerName, action.shouldAdjustClipRange); +} + +export default function* listenToClipHistogramSaga(): Saga { + yield _takeEvery("CLIP_HISTOGRAM", handleClipHistogram); +} diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index b38b378db1a..69aed3e38de 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -310,7 +310,7 @@ function* maybeLoadIsosurface( threeDMap.set(clippedPosition, true); - const voxelDimensions = window.__isosurfaceVoxelDimensions || [4, 4, 4]; + const subsamplingStrides = window.__isosurfaceSubsamplingStrides || [4, 4, 4]; const scale = yield* select(state => state.dataset.dataSource.scale); const dataStoreHost = yield* select(state => state.dataset.dataStore.url); const tracingStoreHost = yield* select(state => state.tracing.tracingStore.url); @@ -344,7 +344,7 @@ function* maybeLoadIsosurface( position: clippedPosition, zoomStep, segmentId, - voxelDimensions, + subsamplingStrides, cubeSize, scale, }, diff --git a/frontend/javascripts/oxalis/model/sagas/load_histogram_data_saga.js b/frontend/javascripts/oxalis/model/sagas/load_histogram_data_saga.js index 2fb027b10c9..8bc5d4cb519 100644 --- a/frontend/javascripts/oxalis/model/sagas/load_histogram_data_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/load_histogram_data_saga.js @@ -30,7 +30,7 @@ async function fetchAllHistogramsForLayers( return histograms; } -export default function* loadHistogramData(): Saga { +export default function* loadHistogramDataSaga(): Saga { yield* take("WK_READY"); // Flow does not understand that Array is returned for some reason. const dataLayers: Array = (yield* call([Model, Model.getColorLayers]): any); diff --git a/frontend/javascripts/oxalis/model/sagas/min_cut_saga.js b/frontend/javascripts/oxalis/model/sagas/min_cut_saga.js new file mode 100644 index 00000000000..dd701db3a4d --- /dev/null +++ b/frontend/javascripts/oxalis/model/sagas/min_cut_saga.js @@ -0,0 +1,761 @@ +// @flow +import _ from "lodash"; + +import type { Action } from "oxalis/model/actions/actions"; +import type { BoundingBoxType, Vector3 } from "oxalis/constants"; +import type { Node } from "oxalis/store"; +import { type Saga, call, put, select } from "oxalis/model/sagas/effect-generators"; +import { V3 } from "libs/mjs"; +import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; +import { + enforceActiveVolumeTracing, + getActiveSegmentationTracingLayer, +} from "oxalis/model/accessors/volumetracing_accessor"; +import { finishAnnotationStrokeAction } from "oxalis/model/actions/volumetracing_actions"; +import { getResolutionInfo } from "oxalis/model/accessors/dataset_accessor"; +import { takeEveryUnlessBusy } from "oxalis/model/sagas/saga_helpers"; +import BoundingBox from "oxalis/model/bucket_data_handling/bounding_box"; +import Toast from "libs/toast"; +import * as Utils from "libs/utils"; +import createProgressCallback from "libs/progress_callback"; +import api from "oxalis/api/internal_api"; +import window from "libs/window"; + +// By default, a new bounding box is created around +// the seed nodes with a padding. Within the bounding box +// the min-cut is computed. +const DEFAULT_PADDING = [50, 50, 50]; + +// Voxels which are close to the seeds must not be relabeled. +// Otherwise, trivial min-cuts are performed which cut right +// around one seed. +// This distance is specified in voxels of the current mag (i.e., +// a distance of 30 vx should be respected) and does not need scaling +// to another mag. +// For seeds that are very close to each other, their distance +// overrides this threshold. +const MIN_DIST_TO_SEED = 30; + +const TimeoutError = new Error("Timeout"); +const PartitionFailedError = new Error( + "Segmentation could not be partioned. Zero edges removed in last iteration. Probably due to nodes being too close to each other? Aborting...", +); + +// If the min-cut does not succeed after 10 seconds +// in the selected mag, the next mag is tried. +const MIN_CUT_TIMEOUT = 10 * 1000; // 10 seconds +// During the refinement phase, the timeout is more forgiving. +// Even if the refinement is slow, we typically don't want to +// abort it, since the initial min-cut has already been performed. +// Note that the timeout is used for each refining min-cut phase. +const MIN_CUT_TIMEOUT_REFINEMENT = 30 * 1000; // 30 seconds + +// To choose the initial mag, a voxel threshold is defined +// as a heuristic. This avoids that an unrealistic mag +// is tried in the first place. +// 2 MV corresponds to ~8MB for uint32 data. +const VOXEL_THRESHOLD = 2000000; + +// The first magnification is always ignored initially as a performance +// optimization (unless it's the only existent mag). +const ALWAYS_IGNORE_FIRST_MAG_INITIALLY = true; + +function selectAppropriateResolutions(boundingBoxMag1, resolutionInfo): Array<[number, Vector3]> { + const resolutionsWithIndices = resolutionInfo.getResolutionsWithIndices(); + const appropriateResolutions = []; + + for (const [resolutionIndex, resolution] of resolutionsWithIndices) { + if ( + resolutionIndex === 0 && + resolutionsWithIndices.length > 1 && + ALWAYS_IGNORE_FIRST_MAG_INITIALLY + ) { + // Don't consider Mag 1, as it's usually too fine-granular + continue; + } + const boundingBoxTarget = boundingBoxMag1.fromMag1ToMag(resolution); + + if (boundingBoxTarget.getVolume() < VOXEL_THRESHOLD) { + appropriateResolutions.push([resolutionIndex, resolution]); + } + } + + return appropriateResolutions; +} + +// +// Helper functions for managing neighbors / edges. +// + +// There are 6 neighbor in 3D space (manhattan-jumps). +// The neighbors can be accessed via neighbor indices (e.g., idx=1 ==> neighbor [0, -1, 0]) +const NEIGHBOR_LOOKUP = [[0, 0, -1], [0, -1, 0], [-1, 0, 0], [0, 0, 1], [0, 1, 0], [1, 0, 0]]; +// neighborToIndex is a mapping from neighbor to neighbor index (e.g., neighbor [0, -1, 0] ==> idx=1) +const neighborToIndex = new Map(_.zip(NEIGHBOR_LOOKUP, _.range(NEIGHBOR_LOOKUP.length))); + +function getNeighborIdx(neighbor) { + const neighborIdx = neighborToIndex.get(neighbor); + if (neighborIdx == null) { + throw new Error("Could not look up neighbor"); + } + return neighborIdx; +} + +// Given a neighbor X (e.g., idx=1 == [0, -1, 0]), the opposite neighbor can be +// interesting (idx=4 == [0, 1, 0]). A common use case is dealing with doubly-linked +// edges in a graph. For example: +// +// Given the neighboring voxels: +// A --> B +// From the perspective of A, B can be accessed by using the outgoing edge +// along vector [0, 1, 0] (idx=4). From the perspective of B, this is an +// ingoing edge (referenced by idx=1 == [0, -1, 0]). +const invertNeighborIdx = (neighborIdx: number) => + (neighborIdx + NEIGHBOR_LOOKUP.length / 2) % NEIGHBOR_LOOKUP.length; + +function _getNeighborsFromBitMask(bitMask) { + // Note: Use the memoized version of this: getNeighborsFromBitMask. + // Ingoing and outgoing edges are stored as a bitmask. The first half + // (higher significance) of the bitmask holds the ingoing edges as bits. + // The second half (lower significance) the outgoing edges. + // + // For example, the bitmask + // 010000 000000 + // (most significant bits to least significant bits) + // means that there is exactly one ingoing edge ([0, -1, 0]). + const neighbors = { + ingoing: [], + outgoing: [], + }; + + for (let neighborIdx = 0; neighborIdx < NEIGHBOR_LOOKUP.length; neighborIdx++) { + if ((bitMask & (2 ** neighborIdx)) !== 0) { + neighbors.outgoing.push(NEIGHBOR_LOOKUP[neighborIdx]); + } + if ((bitMask & (2 ** (neighborIdx + NEIGHBOR_LOOKUP.length))) !== 0) { + neighbors.ingoing.push(NEIGHBOR_LOOKUP[neighborIdx]); + } + } + + return neighbors; +} +const getNeighborsFromBitMask = _.memoize(_getNeighborsFromBitMask); + +// Functions to add/remove edges which mutate the bitmask. +function addOutgoingEdge(edgeBuffer, idx, neighborIdx) { + edgeBuffer[idx] |= 2 ** neighborIdx; +} +function addIngoingEdge(edgeBuffer, idx, neighborIdx) { + edgeBuffer[idx] |= 2 ** (NEIGHBOR_LOOKUP.length + neighborIdx); +} +function removeIngoingEdge(edgeBuffer, idx, neighborIdx) { + edgeBuffer[idx] &= ~(2 ** (NEIGHBOR_LOOKUP.length + neighborIdx)); +} +function removeOutgoingEdge(edgeBuffer, idx, neighborIdx) { + edgeBuffer[idx] &= ~(2 ** neighborIdx); +} + +export function isBoundingBoxUsableForMinCut(boundingBoxObj: BoundingBoxType, nodes: Array) { + const bbox = new BoundingBox(boundingBoxObj); + return bbox.containsPoint(nodes[0].position) && bbox.containsPoint(nodes[1].position); +} + +// +// Actual min cut implementation. +// +// Note that a heuristic is used to +// determine an appropriate mag to compute the min-cut in. +// If the computation takes too long, the min-cut is aborted and a +// poorer mag is tried. +// Afterwards, it is tried to "refine" the min-cut by also calculating +// the min-cut in better mags. As a result, the cut is initially drawn +// "with broad/fast strokes" and the final details are solved in higher +// mags. + +function* performMinCut(action: Action): Saga { + if (action.type !== "PERFORM_MIN_CUT") { + throw new Error("Satisfy flow."); + } + + const skeleton = yield* select(store => store.tracing.skeleton); + if (!skeleton) { + return; + } + const seedTree = skeleton.trees[action.treeId]; + + if (!seedTree) { + throw new Error(`seedTree with id ${action.treeId} not found.`); + } + + const nodes = Array.from(seedTree.nodes.values()); + + if (nodes.length !== 2) { + // The min-cut operation should not be available in the context-menu when the tree + // does not have exactly two nodes. + return; + } + + const { boundingBoxId } = action; + let boundingBoxObj; + if (boundingBoxId != null) { + const boundingBoxes = skeleton.userBoundingBoxes.filter(bbox => bbox.id === boundingBoxId); + if (boundingBoxes.length !== 1) { + throw new Error( + `Expected (exactly) one bounding box with id ${boundingBoxId} but there are ${ + boundingBoxes.length + }.`, + ); + } + boundingBoxObj = boundingBoxes[0].boundingBox; + } else { + const newBBox = { + min: V3.floor(V3.sub(V3.min(nodes[0].position, nodes[1].position), DEFAULT_PADDING)), + max: V3.floor( + V3.add( + V3.add(V3.max(nodes[0].position, nodes[1].position), DEFAULT_PADDING), + // Add [1, 1, 1], since BoundingBox.max is exclusive + [1, 1, 1], + ), + ), + }; + + yield* put( + addUserBoundingBoxAction({ + boundingBox: newBBox, + name: `Bounding box used for splitting cell (seedA=(${nodes[0].position.join( + ",", + )}), seedB=(${nodes[1].position.join(",")}), timestamp=${new Date().getTime()})`, + color: Utils.getRandomColor(), + isVisible: true, + }), + ); + + boundingBoxObj = newBBox; + } + + const boundingBoxMag1 = new BoundingBox(boundingBoxObj); + + if (!isBoundingBoxUsableForMinCut(boundingBoxObj, nodes)) { + // The user should not be able to trigger this path --> Don't show a toast. + // Only keep this for debugging purposes. + console.log("The seeds are not contained in the current bbox."); + return; + } + + const volumeTracingLayer = yield* select(store => getActiveSegmentationTracingLayer(store)); + const volumeTracing = yield* select(enforceActiveVolumeTracing); + if (!volumeTracingLayer) { + console.log("No volumeTracing available."); + return; + } + + const resolutionInfo = getResolutionInfo(volumeTracingLayer.resolutions); + const appropriateResolutionInfos = selectAppropriateResolutions(boundingBoxMag1, resolutionInfo); + if (appropriateResolutionInfos.length === 0) { + yield* call( + [Toast, Toast.warning], + "The bounding box for the selected seeds is too large. Choose a smaller bounding box or lower the distance between the seeds. Alternatively, ensure that lower magnifications exist which can be used.", + ); + return; + } + + const progressCallback = createProgressCallback({ pauseDelay: 200, successMessageDelay: 5000 }); + + // Try to perform a min-cut on the selected resolutions. If the min-cut + // fails for one resolution, it's tried again on the next resolution. + // If the min-cut succeeds, it's refined again with the better resolutions. + for (const [resolutionIndex, targetMag] of appropriateResolutionInfos) { + try { + yield* call( + progressCallback, + false, + `Trying min-cut computation at Mag=${targetMag.join("-")}`, + ); + + console.group("Trying min-cut computation at", targetMag.join("-")); + yield* call( + tryMinCutAtMag, + targetMag, + resolutionIndex, + boundingBoxMag1, + nodes, + volumeTracingLayer, + MIN_CUT_TIMEOUT, + ); + console.groupEnd(); + + for ( + let refiningResolutionIndex = resolutionIndex - 1; + refiningResolutionIndex >= 0; + refiningResolutionIndex-- + ) { + // Refine min-cut on lower resolutions, if they exist. + if (!resolutionInfo.hasIndex(refiningResolutionIndex)) { + continue; + } + + const refiningResolution = resolutionInfo.getResolutionByIndexOrThrow( + refiningResolutionIndex, + ); + console.group("Refining min-cut at", refiningResolution.join("-")); + yield* call( + progressCallback, + false, + `Refining min-cut at Mag=${refiningResolution.join("-")}`, + ); + try { + yield* call( + tryMinCutAtMag, + refiningResolution, + refiningResolutionIndex, + boundingBoxMag1, + nodes, + volumeTracingLayer, + MIN_CUT_TIMEOUT_REFINEMENT, + ); + } catch (exception) { + if (exception === TimeoutError) { + console.warn( + "Refinement of min-cut timed out. There still might be small voxel connections between the seeds.", + ); + yield* put(finishAnnotationStrokeAction(volumeTracing.tracingId)); + yield* call( + progressCallback, + true, + "Min-cut calculation finished. However, the refinement timed out. There still might be small voxel connections between the seeds.", + {}, + "warning", + ); + return; + } + } + console.groupEnd(); + } + + yield* put(finishAnnotationStrokeAction(volumeTracing.tracingId)); + yield* call(progressCallback, true, "Min-cut calculation was successful."); + return; + } catch (exception) { + console.groupEnd(); + if (exception === TimeoutError) { + console.log("Retrying at higher mag if possible..."); + } else if (exception === PartitionFailedError) { + console.warn(exception); + return; + } else { + throw exception; + } + } + } + + yield* call( + progressCallback, + true, + "A min-cut couldn't be performed, because the calculation timed out.", + {}, + "warning", + ); + yield* call([Toast, Toast.warning], "Couldn't perform min-cut due to timeout"); +} + +// +// Algorithmic implementation of the min cut approach. +// For more background, look up the Edmonds–Karp or Ford–Fulkerson algorithm. +// Note that our min-cut approach is a special case of these algorithms, since +// we only have edges with a capacity of 1. +// +// The directed (!) voxel graph is defined so that each voxel is a node and +// two nodes are connected if they are neighbors and have the same segment id. +// Since the graph is directed, all edges are initially symmetric double-edges. +// +// The algorithm looks for shortest paths between two given seeds A and B (via +// breadth-first searches). +// In each iteration, a shortest path between A and B is removed until no paths +// exist anymore. Thus, the two seeds are separated from each other. +// +// When removing a path, only the edges are removed which point from A to B. +// This leaves "back-edges" in the graph (also known as "residuals"). +// In the final phase, these residuals are traversed to find out which nodes +// cannot be reached, anymore. These nodes are the ones that should be erased +// to separate A from B. + +function* tryMinCutAtMag( + targetMag, + resolutionIndex, + boundingBoxMag1, + nodes, + volumeTracingLayer, + maxTimeoutDuration, +): Saga { + const targetMagString = `${targetMag.join(",")}`; + const boundingBoxTarget = boundingBoxMag1.fromMag1ToMag(targetMag); + + const globalSeedA = V3.fromMag1ToMag(nodes[0].position, targetMag); + const globalSeedB = V3.fromMag1ToMag(nodes[1].position, targetMag); + + const minDistToSeed = Math.min(V3.length(V3.sub(globalSeedA, globalSeedB)) / 2, MIN_DIST_TO_SEED); + console.log("Setting minDistToSeed to ", minDistToSeed); + + const seedA = V3.sub(globalSeedA, boundingBoxTarget.min); + const seedB = V3.sub(globalSeedB, boundingBoxTarget.min); + + console.log(`Loading data... (for ${boundingBoxTarget.getVolume()} vx)`); + const inputData = yield* call( + [api.data, api.data.getDataFor2DBoundingBox], + volumeTracingLayer.name, + boundingBoxMag1, + resolutionIndex, + ); + + // For the 3D volume flat arrays are constructed + // which can be accessed with the helper methods + // l(x, y, z) and l([x, y, z]). + const size = boundingBoxTarget.getSize(); + const l = (x, y, z) => z * size[1] * size[0] + y * size[0] + x; + const ll = ([x, y, z]) => z * size[1] * size[0] + y * size[0] + x; + + if (inputData[ll(seedA)] !== inputData[ll(seedB)]) { + yield* call( + [Toast, Toast.warning], + `The given seeds are not placed on same segment: ${inputData[ll(seedA)]} vs ${ + inputData[ll(seedB)] + }.`, + ); + return; + } + + const segmentId = inputData[ll(seedA)]; + + // We calculate the latest time at which the min cut may still + // be executed. The min-cut algorithm will check from time to + // time whether the threshold is violated. + // This approach was chosen over a timeout mechanism via + // race/delay saga due to performance reasons. The latter approach + // requires that even tightly-called functions are generators + // which yield to the redux-saga middleware. Rough benchmarks + // showed an overall slow-down of factor 6. + const timeoutThreshold = performance.now() + maxTimeoutDuration; + console.log("Starting min-cut..."); + console.time(`Total min-cut (${targetMagString})`); + + console.time(`Build Graph (${targetMagString})`); + + const edgeBuffer = buildGraph( + inputData, + segmentId, + size, + boundingBoxTarget.getVolume(), + l, + ll, + timeoutThreshold, + ); + + // Copy original edge buffer, as it's mutated later when removing edges. + const originalEdgeBuffer = new Uint16Array(edgeBuffer); + console.timeEnd(`Build Graph (${targetMagString})`); + + console.time(`Find & delete paths (${targetMagString})`); + + let exitLoop = false; + while (!exitLoop) { + const { foundTarget, distanceField, directionField } = populateDistanceField( + edgeBuffer, + boundingBoxTarget, + seedA, + seedB, + ll, + timeoutThreshold, + ); + if (foundTarget) { + const { removedEdgeCount } = removeShortestPath( + distanceField, + directionField, + seedA, + seedB, + ll, + size, + edgeBuffer, + minDistToSeed, + ); + if (removedEdgeCount === 0) { + throw PartitionFailedError; + } + } else { + console.log("Segmentation is partitioned"); + exitLoop = true; + } + } + + console.timeEnd(`Find & delete paths (${targetMagString})`); + + console.time(`traverseResidualsField (${targetMagString})`); + const { visitedField } = traverseResidualsField(boundingBoxTarget, seedA, ll, edgeBuffer); + console.timeEnd(`traverseResidualsField (${targetMagString})`); + + console.time(`labelDeletedEdges (${targetMagString})`); + labelDeletedEdges(visitedField, boundingBoxTarget, size, originalEdgeBuffer, targetMag, l, ll); + console.timeEnd(`labelDeletedEdges (${targetMagString})`); + + console.timeEnd(`Total min-cut (${targetMagString})`); +} + +function isPositionOutside(position, size) { + return ( + position[0] < 0 || + position[1] < 0 || + position[2] < 0 || + position[0] >= size[0] || + position[1] >= size[1] || + position[2] >= size[2] + ); +} + +function buildGraph(inputData, segmentId, size, length, l, ll, timeoutThreshold) { + const edgeBuffer = new Uint16Array(length); + for (let z = 0; z < size[2]; z++) { + if (z % Math.floor(size[2] / 10) === 0 && performance.now() > timeoutThreshold) { + // After each 10% chunk, we check whether there's a timeout + throw TimeoutError; + } + for (let y = 0; y < size[1]; y++) { + for (let x = 0; x < size[0]; x++) { + // Traverse over all voxels + + const pos = [x, y, z]; + const linIndex = l(x, y, z); + + // Ignore voxel if it does not belong to seed segment + if (inputData[linIndex] !== segmentId) { + continue; + } + + // Go over all neighbors + for (let neighborIdx = 0; neighborIdx < NEIGHBOR_LOOKUP.length; neighborIdx++) { + const neighbor = NEIGHBOR_LOOKUP[neighborIdx]; + const neighborPos = V3.add(pos, neighbor); + + if (isPositionOutside(neighborPos, size)) { + // neighbor is outside of volume + continue; + } + + const neighborLinIndex = ll(neighborPos); + if (inputData[neighborLinIndex] === segmentId) { + addOutgoingEdge(edgeBuffer, linIndex, neighborIdx); + addIngoingEdge(edgeBuffer, neighborLinIndex, invertNeighborIdx(neighborIdx)); + } + } + } + } + } + return edgeBuffer; +} + +function populateDistanceField( + edgeBuffer: Uint16Array, + boundingBoxTarget: BoundingBox, + seedA: Vector3, + seedB: Vector3, + ll, + timeoutThreshold: number, +) { + // Perform a breadth-first search from seedA to seedB. + + // The distance field encodes the distance from the current voxel to seedA. + const distanceField = new Uint16Array(boundingBoxTarget.getVolume()); + + // The direction field encodes for each voxel which direction needs to be + // taken to follow the shortest path to seedA. Later, this field is used + // to remove a shortest path. + const directionField = new Uint8Array(boundingBoxTarget.getVolume()).fill(255); + + const queue: Array<{ voxel: Vector3, distance: number, usedEdgeIdx: number }> = [ + { voxel: seedA, distance: 1, usedEdgeIdx: 255 }, + ]; + let foundTarget = false; + let lastDistance = 0; + let iterationCount = 0; + + while (queue.length > 0) { + iterationCount++; + if (iterationCount % 10000 === 0 && performance.now() > timeoutThreshold) { + throw TimeoutError; + } + const { voxel: currVoxel, distance, usedEdgeIdx } = queue.shift(); + + const currVoxelIdx = ll(currVoxel); + if (distanceField[currVoxelIdx] > 0) { + continue; + } + + if (distance > lastDistance) { + lastDistance = distance; + } + + distanceField[currVoxelIdx] = distance; + directionField[currVoxelIdx] = usedEdgeIdx; + + if (V3.equals(currVoxel, seedB)) { + foundTarget = true; + break; + } + + const neighbors = getNeighborsFromBitMask(edgeBuffer[currVoxelIdx]).outgoing; + for (const neighbor of neighbors) { + const neighborPos = V3.add(currVoxel, neighbor); + const neighborIdx = getNeighborIdx(neighbor); + + if (distanceField[ll(neighborPos)] === 0) { + queue.push({ voxel: neighborPos, distance: distance + 1, usedEdgeIdx: neighborIdx }); + } + } + } + + return { distanceField, directionField, foundTarget }; +} + +function removeShortestPath( + distanceField: Uint16Array, + directionField: Uint8Array, + seedA, + seedB, + ll, + size, + edgeBuffer, + minDistToSeed, +) { + // Extract shortest path from seedB to seedA and remove edges which + // belong to that path. + const path = []; + let foundSeed = false; + const voxelStack = [seedB]; + const maxDistance = distanceField[ll(seedB)]; + let removedEdgeCount = 0; + + while (voxelStack.length > 0) { + const currentVoxel = voxelStack.pop(); + const currentDistance = distanceField[ll(currentVoxel)]; + + if (V3.equals(currentVoxel, seedA)) { + foundSeed = true; + break; + } + + const originallyUsedEdgeId = directionField[ll(currentVoxel)]; + if (originallyUsedEdgeId >= NEIGHBOR_LOOKUP.length) { + throw new Error("Could not look up used edge in directionField"); + } + const neighborIdx = invertNeighborIdx(originallyUsedEdgeId); + const neighbor = NEIGHBOR_LOOKUP[neighborIdx]; + + const neighborPos = V3.add(currentVoxel, neighbor); + + if (isPositionOutside(neighborPos, size)) { + throw new Error("Neighbor is outside of volume?"); + } + + if (!(distanceField[ll(neighborPos)] < currentDistance && distanceField[ll(neighborPos)] > 0)) { + throw new Error("Direction points towards an equal/higher/zero distance?"); + } + + const currDist = distanceField[ll(neighborPos)]; + const distToSeed = Math.min(currDist, maxDistance - currDist); + + // We don't want to remove voxels which are close to the seeds. + // See explanation for MIN_DIST_TO_SEED for details. + if (distToSeed > minDistToSeed) { + removedEdgeCount++; + path.unshift(neighborPos); + + // Remove ingoing edge + removeIngoingEdge(edgeBuffer, ll(currentVoxel), neighborIdx); + + // Remove outgoing edge + const invertedNeighborIdx = invertNeighborIdx(neighborIdx); + removeOutgoingEdge(edgeBuffer, ll(neighborPos), invertedNeighborIdx); + } + + voxelStack.push(neighborPos); + } + + return { path, foundSeed, removedEdgeCount }; +} + +function traverseResidualsField( + boundingBoxTarget: BoundingBox, + seedA: Vector3, + ll, + edgeBuffer: Uint16Array, +) { + // Perform a breadth-first search from seedA and return + // which voxels were visited (visitedField). This represents + // the graph component for seedA. + const visitedField = new Uint8Array(boundingBoxTarget.getVolume()); + const queue: Array = [seedA]; + while (queue.length > 0) { + const currVoxel = queue.shift(); + + const currVoxelIdx = ll(currVoxel); + if (visitedField[currVoxelIdx] > 0) { + continue; + } + visitedField[currVoxelIdx] = 1; + const neighbors = getNeighborsFromBitMask(edgeBuffer[currVoxelIdx]).outgoing; + + for (const neighbor of neighbors) { + const neighborPos = V3.add(currVoxel, neighbor); + + queue.push(neighborPos); + } + } + + return { visitedField }; +} + +function labelDeletedEdges( + visitedField, + boundingBoxTarget, + size, + originalEdgeBuffer, + targetMag, + l, + ll, +) { + for (let z = 0; z < size[2]; z++) { + for (let y = 0; y < size[1]; y++) { + for (let x = 0; x < size[0]; x++) { + const idx = l(x, y, z); + if (visitedField[idx] === 1) { + const neighbors = getNeighborsFromBitMask(originalEdgeBuffer[idx]).outgoing; + const currentPos = [x, y, z]; + + for (const neighbor of neighbors) { + const neighborPos = V3.add(currentPos, neighbor); + + if (visitedField[ll(neighborPos)] === 0) { + const position = V3.fromMagToMag1( + V3.add(boundingBoxTarget.min, neighborPos), + targetMag, + ); + for (let dz = 0; dz < targetMag[2]; dz++) { + for (let dy = 0; dy < targetMag[1]; dy++) { + for (let dx = 0; dx < targetMag[0]; dx++) { + api.data.labelVoxels([V3.add(position, [dx, dy, dz])], 0); + } + } + } + + if (window.visualizeRemovedVoxelsOnMinCut) { + window.addVoxelMesh(position, targetMag); + } + } + } + } + } + } + } +} + +export default function* listenToMinCut(): Saga { + yield* takeEveryUnlessBusy("PERFORM_MIN_CUT", performMinCut, "Min-cut is being computed."); +} diff --git a/frontend/javascripts/oxalis/model/sagas/root_saga.js b/frontend/javascripts/oxalis/model/sagas/root_saga.js index 070a349a45a..f65380bf819 100644 --- a/frontend/javascripts/oxalis/model/sagas/root_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/root_saga.js @@ -17,7 +17,8 @@ import { watchMaximumRenderableLayers } from "oxalis/model/sagas/dataset_saga"; import { watchToolDeselection } from "oxalis/model/sagas/annotation_tool_saga"; import SettingsSaga from "oxalis/model/sagas/settings_saga"; import watchTasksAsync, { warnAboutMagRestriction } from "oxalis/model/sagas/task_saga"; -import HistogramSaga from "oxalis/model/sagas/load_histogram_data_saga"; +import loadHistogramDataSaga from "oxalis/model/sagas/load_histogram_data_saga"; +import listenToClipHistogramSaga from "oxalis/model/sagas/clip_histogram_saga"; import MappingSaga from "oxalis/model/sagas/mapping_saga"; let rootSagaCrashed = false; @@ -40,7 +41,8 @@ function* restartableSaga(): Saga { _call(warnAboutMagRestriction), _call(SettingsSaga), _call(watchSkeletonTracingAsync), - _call(HistogramSaga), + _call(listenToClipHistogramSaga), + _call(loadHistogramDataSaga), _call(watchDataRelevantChanges), _call(isosurfaceSaga), _call(watchTasksAsync), diff --git a/frontend/javascripts/oxalis/model/sagas/saga_helpers.js b/frontend/javascripts/oxalis/model/sagas/saga_helpers.js new file mode 100644 index 00000000000..b0ad12e9c3d --- /dev/null +++ b/frontend/javascripts/oxalis/model/sagas/saga_helpers.js @@ -0,0 +1,41 @@ +// @flow + +import type { Action } from "oxalis/model/actions/actions"; +import { type Saga, call, put, select, _takeEvery } from "oxalis/model/sagas/effect-generators"; +import { type Pattern } from "redux-saga"; +import { setBusyBlockingInfoAction } from "oxalis/model/actions/ui_actions"; + +export function* takeEveryUnlessBusy( + actionDescriptor: Pattern, + saga: Action => Saga, + reason: string, +): Saga { + /* + * Similar to _takeEvery, this function can be used to react to + * actions to start sagas. However, the difference is that once the given + * saga is executed, webKnossos will be marked as busy. When being busy, + * following actions which match the actionDescriptor are ignored. + * When the given saga finishes, busy is set to false. + * + * Note that busyBlockingInfo is also used/respected in other places within + * webKnossos. + */ + + function* sagaBusyWrapper(action: Action) { + const busyBlockingInfo = yield* select(state => state.uiInformation.busyBlockingInfo); + if (busyBlockingInfo.isBusy) { + console.warn( + `Ignoring ${action.type} request (reason: ${busyBlockingInfo.reason || "null"})`, + ); + return; + } + + yield* put(setBusyBlockingInfoAction(true, reason)); + yield* call(saga, action); + yield* put(setBusyBlockingInfoAction(false)); + } + + yield _takeEvery(actionDescriptor, sagaBusyWrapper); +} + +export default {}; diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.js b/frontend/javascripts/oxalis/model/sagas/save_saga.js index 32ce320bfb5..1871990c331 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.js @@ -83,7 +83,10 @@ import { getVolumeTracings, } from "oxalis/model/accessors/volumetracing_accessor"; import { globalPositionToBucketPosition } from "oxalis/model/helpers/position_converter"; -import { maybeGetSomeTracing, selectTracing } from "oxalis/model/accessors/tracing_accessor"; +import { + getUserBoundingBoxesFromState, + selectTracing, +} from "oxalis/model/accessors/tracing_accessor"; import { selectQueue } from "oxalis/model/accessors/save_accessor"; import { setBusyBlockingInfoAction } from "oxalis/model/actions/ui_actions"; import Date from "libs/date"; @@ -178,11 +181,6 @@ function unpackRelevantActionForUndo(action): RelevantActionsForUndoRedo { throw new Error("Could not unpack redux action from channel"); } -const getUserBoundingBoxesFromState = state => { - const maybeSomeTracing = maybeGetSomeTracing(state.tracing); - return maybeSomeTracing != null ? maybeSomeTracing.userBoundingBoxes : []; -}; - export function* collectUndoStates(): Saga { const undoStack: Array = []; const redoStack: Array = []; @@ -664,18 +662,30 @@ export function* pushTracingTypeAsync( // Save queue is empty, wait for push event yield* take("PUSH_SAVE_QUEUE_TRANSACTION"); } - yield* race({ + const { forcePush } = yield* race({ timeout: _delay(PUSH_THROTTLE_TIME), forcePush: _take("SAVE_NOW"), }); yield* put(setSaveBusyAction(true, tracingType)); - while (true) { - // Send batches to the server until the save queue is empty + if (forcePush) { + while (true) { + // Send batches to the server until the save queue is empty. + saveQueue = yield* select(state => selectQueue(state, tracingType, tracingId)); + if (saveQueue.length > 0) { + yield* call(sendRequestToServer, tracingType, tracingId); + } else { + break; + } + } + } else { saveQueue = yield* select(state => selectQueue(state, tracingType, tracingId)); if (saveQueue.length > 0) { + // Saving the tracing automatically (via timeout) only saves the current state. + // It does not require to reach an empty saveQueue. This is especially + // important when the auto-saving happens during continuous movements. + // Always draining the save queue completely would mean that save + // requests are sent as long as the user moves. yield* call(sendRequestToServer, tracingType, tracingId); - } else { - break; } } yield* put(setSaveBusyAction(false, tracingType)); diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js index cd9c48ca7bc..60c597a4ab5 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.js @@ -4,15 +4,8 @@ import { message } from "antd"; import React from "react"; import _ from "lodash"; +import type { Action } from "oxalis/model/actions/actions"; import { CONTOUR_COLOR_DELETE, CONTOUR_COLOR_NORMAL } from "oxalis/geometries/contourgeometry"; -import { - type CopySegmentationLayerAction, - updateDirectionAction, - updateSegmentAction, - finishAnnotationStrokeAction, - type SetActiveCellAction, - type ClickSegmentAction, -} from "oxalis/model/actions/volumetracing_actions"; import { ResolutionInfo, getBoundaries, @@ -69,6 +62,14 @@ import { } from "oxalis/model/accessors/tool_accessor"; import { markVolumeTransactionEnd } from "oxalis/model/bucket_data_handling/bucket"; import { setToolAction, setBusyBlockingInfoAction } from "oxalis/model/actions/ui_actions"; +import { takeEveryUnlessBusy } from "oxalis/model/sagas/saga_helpers"; +import { + updateDirectionAction, + updateSegmentAction, + finishAnnotationStrokeAction, + type SetActiveCellAction, + type ClickSegmentAction, +} from "oxalis/model/actions/volumetracing_actions"; import { updateTemporarySettingAction, type UpdateTemporarySettingAction, @@ -102,13 +103,18 @@ import getSceneController from "oxalis/controller/scene_controller_provider"; import inferSegmentInViewport, { getHalfViewportExtents, } from "oxalis/model/sagas/automatic_brush_saga"; +import listenToMinCut from "oxalis/model/sagas/min_cut_saga"; import sampleVoxelMapToResolution, { applyVoxelMap, } from "oxalis/model/volumetracing/volume_annotation_sampling"; export function* watchVolumeTracingAsync(): Saga { yield* take("WK_READY"); - yield _takeEvery("COPY_SEGMENTATION_LAYER", copySegmentationLayer); + yield* takeEveryUnlessBusy( + "COPY_SEGMENTATION_LAYER", + copySegmentationLayer, + "Copying from neighbor slice", + ); yield _takeLeading("INFER_SEGMENT_IN_VIEWPORT", inferSegmentInViewport); yield* fork(warnOfTooLowOpacity); } @@ -423,7 +429,10 @@ function* labelWithVoxelBuffer2D( ); } -function* copySegmentationLayer(action: CopySegmentationLayerAction): Saga { +function* copySegmentationLayer(action: Action): Saga { + if (action.type !== "COPY_SEGMENTATION_LAYER") { + throw new Error("Satisfy flow"); + } const allowUpdate = yield* select(state => state.tracing.restrictions.allowUpdate); if (!allowUpdate) return; @@ -581,7 +590,7 @@ export function* floodFill(): Saga { yield* put(setBusyBlockingInfoAction(true, "Floodfill is being computed.")); - const currentViewportBounding = yield* call(getBoundingBoxForFloodFill, seedPosition, planeId); + const boundingBoxForFloodFill = yield* call(getBoundingBoxForFloodFill, seedPosition, planeId); const progressCallback = createProgressCallback({ pauseDelay: 200, @@ -604,7 +613,7 @@ export function* floodFill(): Saga { seedPosition, activeCellId, dimensionIndices, - currentViewportBounding, + boundingBoxForFloodFill, labeledZoomStep, progressCallback, fillMode === FillModeEnum._3D, @@ -1006,6 +1015,7 @@ export default [ watchVolumeTracingAsync, maintainSegmentsMap, maintainHoveredSegmentId, + listenToMinCut, maintainContourGeometry, maintainVolumeTransactionEnds, ]; diff --git a/frontend/javascripts/oxalis/view/components/setting_input_views.js b/frontend/javascripts/oxalis/view/components/setting_input_views.js index 3229401a136..860684fdcec 100644 --- a/frontend/javascripts/oxalis/view/components/setting_input_views.js +++ b/frontend/javascripts/oxalis/view/components/setting_input_views.js @@ -507,15 +507,9 @@ export class ColorSetting extends React.PureComponent { style = style || {}; return (
diff --git a/frontend/javascripts/oxalis/view/context_menu.js b/frontend/javascripts/oxalis/view/context_menu.js index 880e211c256..9a8051d25d1 100644 --- a/frontend/javascripts/oxalis/view/context_menu.js +++ b/frontend/javascripts/oxalis/view/context_menu.js @@ -1,7 +1,18 @@ // @flow -import React, { useEffect, type Node } from "react"; -import { Menu, notification, Divider, Tooltip, Popover, Input } from "antd"; import { CopyOutlined } from "@ant-design/icons"; +import type { Dispatch } from "redux"; +import { Dropdown, Menu, notification, Tooltip, Popover, Input } from "antd"; +import { connect, useDispatch, useSelector } from "react-redux"; +import React, { useEffect, type Node } from "react"; + +import type { APIDataset, APIDataLayer, APIMeshFile } from "types/api_flow_types"; +import type { + ActiveMappingInfo, + OxalisState, + SkeletonTracing, + UserBoundingBox, + VolumeTracing, +} from "oxalis/store"; import { type AnnotationTool, AnnotationToolEnum, @@ -9,20 +20,13 @@ import { type OrthoView, VolumeTools, } from "oxalis/constants"; - -import type { OxalisState, SkeletonTracing, VolumeTracing, ActiveMappingInfo } from "oxalis/store"; -import type { APIDataset, APIDataLayer, APIMeshFile } from "types/api_flow_types"; -import { maybeGetSomeTracing } from "oxalis/model/accessors/tracing_accessor"; -import type { Dispatch } from "redux"; -import { connect, useDispatch, useSelector } from "react-redux"; import { V3 } from "libs/mjs"; -import { changeActiveIsosurfaceCellAction } from "oxalis/model/actions/segmentation_actions"; import { addUserBoundingBoxAction, deleteUserBoundingBoxAction, changeUserBoundingBoxAction, } from "oxalis/model/actions/annotation_actions"; -import { type UserBoundingBox } from "oxalis/store"; +import { changeActiveIsosurfaceCellAction } from "oxalis/model/actions/segmentation_actions"; import { deleteEdgeAction, mergeTreesAction, @@ -33,45 +37,51 @@ import { createBranchPointAction, deleteBranchpointByIdAction, } from "oxalis/model/actions/skeletontracing_actions"; -import { - hasAgglomerateMapping, - loadAgglomerateSkeletonAtPosition, -} from "oxalis/controller/combinations/segmentation_handlers"; -import { setWaypoint } from "oxalis/controller/combinations/skeleton_handlers"; -import { setActiveCellAction } from "oxalis/model/actions/volumetracing_actions"; +import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils"; import { getActiveSegmentationTracing } from "oxalis/model/accessors/volumetracing_accessor"; +import { getNodeAndTree, findTreeByNodeId } from "oxalis/model/accessors/skeletontracing_accessor"; +import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; import { getSegmentIdForPosition, handleFloodFillFromGlobalPosition, } from "oxalis/controller/combinations/volume_handlers"; +import { + getVisibleSegmentationLayer, + getMappingInfo, +} from "oxalis/model/accessors/dataset_accessor"; +import { + hasAgglomerateMapping, + loadAgglomerateSkeletonAtPosition, +} from "oxalis/controller/combinations/segmentation_handlers"; +import { isBoundingBoxUsableForMinCut } from "oxalis/model/sagas/min_cut_saga"; import { loadMeshFromFile, maybeFetchMeshFiles, withMappingActivationConfirmation, } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; +import { maybeGetSomeTracing } from "oxalis/model/accessors/tracing_accessor"; +import { + performMinCutAction, + setActiveCellAction, +} from "oxalis/model/actions/volumetracing_actions"; +import { roundTo, hexToRgb, rgbToHex } from "libs/utils"; +import { setWaypoint } from "oxalis/controller/combinations/skeleton_handlers"; import Model from "oxalis/model"; -import api from "oxalis/api/internal_api"; +import Shortcut from "libs/shortcut_component"; import Toast from "libs/toast"; +import api from "oxalis/api/internal_api"; import messages from "messages"; -import { - getVisibleSegmentationLayer, - getMappingInfo, -} from "oxalis/model/accessors/dataset_accessor"; -import { getNodeAndTree, findTreeByNodeId } from "oxalis/model/accessors/skeletontracing_accessor"; -import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils"; -import { roundTo, hexToRgb, rgbToHex } from "libs/utils"; -import Shortcut from "libs/shortcut_component"; -import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; +const { SubMenu } = Menu; /* eslint-disable react/no-unused-prop-types */ // The newest eslint version thinks the props listed below aren't used. type OwnProps = {| - contextMenuPosition: [number, number], - clickedNodeId: ?number, + contextMenuPosition: ?[number, number], + maybeClickedNodeId: ?number, clickedBoundingBoxId: ?number, globalPosition: Vector3, - viewport: OrthoView, + maybeViewport: ?OrthoView, hideContextMenu: () => void, |}; @@ -108,14 +118,21 @@ type StateProps = {| mappingInfo: ActiveMappingInfo, |}; -/* eslint-enable react/no-unused-prop-types */ - type Props = {| ...OwnProps, ...StateProps, ...DispatchProps |}; -type NodeContextMenuOptionsProps = {| ...Props, clickedNodeId: number |}; +type PropsWithRef = {| ...Props, inputRef: React$ElementRef<*> |}; +type NodeContextMenuOptionsProps = {| + ...Props, + viewport: OrthoView, + clickedNodeId: number, + infoRows: Array, +|}; +/* eslint-enable react/no-unused-prop-types */ type NoNodeContextMenuProps = {| ...Props, + viewport: OrthoView, segmentIdAtPosition: number, activeTool: AnnotationTool, + infoRows: Array, |}; const MenuItemWithMappingActivationConfirmation = withMappingActivationConfirmation(Menu.Item); @@ -239,6 +256,48 @@ function shortcutBuilder(shortcuts: Array): Node { ); } +function getMaybeMinCutItem(clickedTree, volumeTracing, userBoundingBoxes, dispatch) { + const seeds = Array.from(clickedTree.nodes.values()); + if (volumeTracing == null || seeds.length !== 2) { + return null; + } + + return ( + + + dispatch(performMinCutAction(clickedTree.treeId))} + > + Use default bounding box + + + {userBoundingBoxes + .filter(bbox => isBoundingBoxUsableForMinCut(bbox.boundingBox, seeds)) + .map(bbox => ( + dispatch(performMinCutAction(clickedTree.treeId, bbox.id))} + > + {bbox.name || "Unnamed bounding box"} + + ))} + + + ); +} + function NodeContextMenuOptions({ skeletonTracing, clickedNodeId, @@ -251,10 +310,14 @@ function NodeContextMenuOptions({ setActiveNode, hideTree, useLegacyBindings, + volumeTracing, + infoRows, }: NodeContextMenuOptionsProps) { + const dispatch = useDispatch(); if (skeletonTracing == null) { return null; } + const { userBoundingBoxes } = skeletonTracing; const { activeTreeId, trees, activeNodeId } = skeletonTracing; const clickedTree = findTreeByNodeId(trees, clickedNodeId).get(); const areInSameTree = activeTreeId === clickedTree.treeId; @@ -270,15 +333,14 @@ function NodeContextMenuOptions({ return ( setActiveNode(clickedNodeId)} > Select this Node + {getMaybeMinCutItem(clickedTree, volumeTracing, userBoundingBoxes, dispatch)} (activeNodeId != null ? mergeTrees(clickedNodeId, activeNodeId) : null)} @@ -287,7 +349,6 @@ function NodeContextMenuOptions({ {useLegacyBindings ? shortcutBuilder(["Shift", "Alt", "leftMouse"]) : null} (activeNodeId != null ? deleteEdge(activeNodeId, clickedNodeId) : null)} @@ -295,11 +356,7 @@ function NodeContextMenuOptions({ Delete Edge to this Node{" "} {useLegacyBindings ? shortcutBuilder(["Shift", "Ctrl", "leftMouse"]) : null} - deleteNode(clickedNodeId, clickedTree.treeId)} - > + deleteNode(clickedNodeId, clickedTree.treeId)}> Delete this Node {activeNodeId === clickedNodeId ? shortcutBuilder(["Del"]) : null} {isBranchpoint ? ( @@ -323,32 +380,29 @@ function NodeContextMenuOptions({ Mark as Branchpoint {activeNodeId === clickedNodeId ? shortcutBuilder(["B"]) : null} )} + {isTheSameNode ? null : ( + + activeNodeId != null + ? measureAndShowLengthBetweenNodes(activeNodeId, clickedNodeId) + : null + } + > + Path Length to this Node + + )} - activeNodeId != null - ? measureAndShowLengthBetweenNodes(activeNodeId, clickedNodeId) - : null - } - > - Path Length to this Node - - measureAndShowFullTreeLength(clickedTree.treeId, clickedTree.name)} > Path Length of this Tree - hideTree(clickedTree.treeId)} - > + hideTree(clickedTree.treeId)}> Hide this Tree + {infoRows} ); } @@ -368,7 +422,6 @@ function getBoundingBoxMenuOptions({ const isBoundingBoxToolActive = activeTool === AnnotationToolEnum.BOUNDING_BOX; const newBoundingBoxMenuItem = ( { addNewBoundingBox(globalPosition); @@ -394,7 +447,7 @@ function getBoundingBoxMenuOptions({ const upscaledBBoxColor = ((hoveredBBox.color.map(colorPart => colorPart * 255): any): Vector3); return [ newBoundingBoxMenuItem, - + , - + Change Bounding Box Color , { hideBoundingBox(clickedBoundingBoxId); @@ -458,7 +510,6 @@ function getBoundingBoxMenuOptions({ Hide Bounding Box , { deleteBoundingBox(clickedBoundingBoxId); @@ -485,6 +536,7 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { setActiveCell, mappingInfo, zoomStep, + infoRows, } = props; const dispatch = useDispatch(); @@ -535,15 +587,10 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { const skeletonActions = skeletonTracing != null ? [ - setWaypoint(globalPosition, viewport, false)} - > + setWaypoint(globalPosition, viewport, false)}> Create Node here , { createTree(); @@ -555,7 +602,6 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { , loadAgglomerateSkeletonAtPosition(globalPosition)} @@ -574,7 +620,6 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { const layerName = visibleSegmentationLayer != null ? visibleSegmentationLayer.name : null; const loadPrecomputedMeshItem = ( + Compute Mesh (ad-hoc) ); @@ -603,7 +644,6 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { // would be an eraser effectively). segmentIdAtPosition > 0 ? ( { setActiveCell(segmentIdAtPosition, globalPosition); @@ -616,7 +656,6 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { loadPrecomputedMeshItem, computeMeshAdHocItem, handleFloodFillFromGlobalPosition(globalPosition, viewport)} > @@ -648,11 +687,26 @@ function NoNodeContextMenuOptions(props: NoNodeContextMenuProps) { return ( {allActions} + {infoRows} ); } -function ContextMenu(props: Props) { +function ContextMenuContainer(props: Props) { + /* + * This container for the context menu is *always* rendered. + * An input ref is stored for the actual container which is + * passed to antd so that it renders the actual + * menu into that container when desired. + * When is not used to render the menu, antd assumes + * that the is a navigational menu. Navigational menus + * behave differently (predominantly, their styling uses way more + * padding and entries are rendered as links). In earlier implementations + * of the context menu, we simply overrode those styles. However, at the + * latest when sub menus are used, the styling issues become too complicated + * to deal with. + */ + const inputRef = React.useRef(null); React.useEffect(() => { @@ -661,133 +715,196 @@ function ContextMenu(props: Props) { } }, [inputRef.current]); + const { contextMenuPosition, hideContextMenu } = props; + + return ( + <> + +
{ + evt.preventDefault(); + hideContextMenu(); + }} + style={{ display: contextMenuPosition == null ? "none" : "inherit" }} + /> + {/* + This div serves as a "prison" for the sticky context menu. The above div + cannot be used since the context menu would then be closed on every click. + Since both divs are absolutely positioned and cover the whole page, + avoid blocking click events by temporarily disabling them for this "prison" + div. + */} +
+
+ +
+ + + ); +} + +function ContextMenuInner(propsWithInputRef: PropsWithRef) { + const { inputRef, ...props } = propsWithInputRef; const { skeletonTracing, activeTool, - clickedNodeId, + maybeClickedNodeId, contextMenuPosition, hideContextMenu, datasetScale, globalPosition, + maybeViewport, } = props; - const activeTreeId = skeletonTracing != null ? skeletonTracing.activeTreeId : null; - const activeNodeId = skeletonTracing != null ? skeletonTracing.activeNodeId : null; - - let nodeContextMenuTree = null; - let nodeContextMenuNode = null; - if (skeletonTracing != null && clickedNodeId != null) { - getNodeAndTree(skeletonTracing, clickedNodeId).map(([tree, node]) => { - nodeContextMenuNode = node; - nodeContextMenuTree = tree; - }); - } - const positionToMeasureDistanceTo = - nodeContextMenuNode != null ? nodeContextMenuNode.position : globalPosition; - const activeNode = - activeNodeId != null && skeletonTracing != null - ? getNodeAndTree(skeletonTracing, activeNodeId, activeTreeId).get()[1] - : null; - const distanceToSelection = - activeNode != null - ? [ - formatNumberToLength( - V3.scaledDist(activeNode.position, positionToMeasureDistanceTo, datasetScale), - ), - formatLengthAsVx(V3.length(V3.sub(activeNode.position, positionToMeasureDistanceTo))), - ] - : null; - const nodePositionAsString = - nodeContextMenuNode != null ? positionToString(nodeContextMenuNode.position) : ""; - const segmentIdAtPosition = getSegmentIdForPosition(globalPosition); + let overlay =
; + if (contextMenuPosition != null && maybeViewport != null) { + const activeTreeId = skeletonTracing != null ? skeletonTracing.activeTreeId : null; + const activeNodeId = skeletonTracing != null ? skeletonTracing.activeNodeId : null; + + let nodeContextMenuTree = null; + let nodeContextMenuNode = null; + if (skeletonTracing != null && maybeClickedNodeId != null) { + getNodeAndTree(skeletonTracing, maybeClickedNodeId).map(([tree, node]) => { + nodeContextMenuNode = node; + nodeContextMenuTree = tree; + }); + } + const positionToMeasureDistanceTo = + nodeContextMenuNode != null ? nodeContextMenuNode.position : globalPosition; + const activeNode = + activeNodeId != null && skeletonTracing != null + ? getNodeAndTree(skeletonTracing, activeNodeId, activeTreeId).get()[1] + : null; + const distanceToSelection = + activeNode != null + ? [ + formatNumberToLength( + V3.scaledDist(activeNode.position, positionToMeasureDistanceTo, datasetScale), + ), + formatLengthAsVx(V3.length(V3.sub(activeNode.position, positionToMeasureDistanceTo))), + ] + : null; + const nodePositionAsString = + nodeContextMenuNode != null ? positionToString(nodeContextMenuNode.position) : ""; + + const segmentIdAtPosition = getSegmentIdForPosition(globalPosition); + + const infoRows = []; + + if (maybeClickedNodeId != null && nodeContextMenuTree != null) { + infoRows.push( +
+ Node with Id {maybeClickedNodeId} in Tree {nodeContextMenuTree.treeId} +
, + ); + } + if (nodeContextMenuNode != null) { + infoRows.push( +
+ Position: {nodePositionAsString} + {copyIconWithTooltip(nodePositionAsString, "Copy node position")} +
, + ); + } else { + const positionAsString = positionToString(globalPosition); + infoRows.push( +
+ Position: {positionAsString} + {copyIconWithTooltip(positionAsString, "Copy position")} +
, + ); + } - const infoRows = []; + if (distanceToSelection != null) { + infoRows.push( +
+ {distanceToSelection[0]} ({distanceToSelection[1]}) to this{" "} + {maybeClickedNodeId != null ? "Node" : "Position"} + {copyIconWithTooltip(distanceToSelection[0], "Copy the distance")} +
, + ); + } - if (clickedNodeId != null && nodeContextMenuTree != null) { - infoRows.push( -
- Node with Id {clickedNodeId} in Tree {nodeContextMenuTree.treeId} -
, - ); - } - if (nodeContextMenuNode != null) { - infoRows.push( -
- Position: {nodePositionAsString} - {copyIconWithTooltip(nodePositionAsString, "Copy node position")} -
, - ); - } else { - const positionAsString = positionToString(globalPosition); - infoRows.push( -
- Position: {positionAsString} - {copyIconWithTooltip(positionAsString, "Copy position")} -
, - ); - } + if (segmentIdAtPosition > 0) { + infoRows.push( +
+
+ Segment ID: {segmentIdAtPosition}{" "} + {copyIconWithTooltip(segmentIdAtPosition, "Copy Segment ID")} +
, + ); + } - if (distanceToSelection != null) { - infoRows.push( -
- {distanceToSelection[0]} ({distanceToSelection[1]}) to this{" "} - {clickedNodeId != null ? "Node" : "Position"} - {copyIconWithTooltip(distanceToSelection[0], "Copy the distance")} -
, - ); - } + const maybeHoveredCellMenuItem = getMaybeHoveredCellMenuItem(globalPosition); + if (!maybeHoveredCellMenuItem) { + infoRows.push(maybeHoveredCellMenuItem); + } - if (segmentIdAtPosition > 0) { - infoRows.push( -
-
- Segment ID: {segmentIdAtPosition}{" "} - {copyIconWithTooltip(segmentIdAtPosition, "Copy Segment ID")} -
, + infoRows.unshift( + , ); - } - const maybeHoveredCellMenuItem = getMaybeHoveredCellMenuItem(globalPosition); - if (!maybeHoveredCellMenuItem) { - infoRows.push(maybeHoveredCellMenuItem); + // It's important to not use + // or + // for the following two expressions, since this breaks + // antd's internal population of the correct class names + // for the menu. + overlay = + maybeClickedNodeId != null + ? NodeContextMenuOptions({ + clickedNodeId: maybeClickedNodeId, + infoRows, + viewport: maybeViewport, + ...props, + }) + : NoNodeContextMenuOptions({ + activeTool, + segmentIdAtPosition, + infoRows, + viewport: maybeViewport, + ...props, + }); } return ( -
{ - evt.preventDefault(); - hideContextMenu(); - }} - /> - {/* This div serves as a "prison" for the sticky context menu. The above div - cannot be used since the context menu would then be closed on every click. - Since both divs are absolutely positioned and cover the whole page, - avoid blocking click events by temporarily disabling them for this "prison" div. */} -
-
+ {inputRef != null ? ( + <> - {clickedNodeId != null - ? NodeContextMenuOptions({ ...props, clickedNodeId }) - : NoNodeContextMenuOptions({ activeTool, segmentIdAtPosition, ...props })} - - - {infoRows} -
-
+ inputRef.current} + destroyPopupOnHide + > +
+ + + ) : null} ); } @@ -866,4 +983,4 @@ function mapStateToProps(state: OxalisState): StateProps { export default connect( mapStateToProps, mapDispatchToProps, -)(ContextMenu); +)(ContextMenuContainer); diff --git a/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.js b/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.js index 035d0acf20f..83eefa115c2 100644 --- a/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.js +++ b/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.js @@ -68,6 +68,8 @@ type State = { model: Model, }; +const ignoredLayoutChangesByAnalytics = ["FlexLayout_SetActiveTabset", "FlexLayout_SelectTab"]; + class FlexLayoutWrapper extends React.PureComponent { unbindListeners: Array<() => void>; // This variable stores the border open status that should be active, when no main tab is maximized. @@ -89,6 +91,13 @@ class FlexLayoutWrapper extends React.PureComponent { componentDidUpdate(prevProps: Props) { const { layoutName, layoutKey } = this.props; if (layoutName !== prevProps.layoutName || layoutKey !== prevProps.layoutKey) { + sendAnalyticsEvent("switched_layout", { + from: { viewMode: prevProps.layoutKey, layoutName: prevProps.layoutName }, + to: { + viewMode: layoutKey, + layoutName, + }, + }); this.rebuildLayout(); } } @@ -118,9 +127,6 @@ class FlexLayoutWrapper extends React.PureComponent { loadCurrentModel() { const { layoutName, layoutKey } = this.props; - if (layoutName !== DEFAULT_LAYOUT_NAME) { - sendAnalyticsEvent("load_custom_layout", { viewMode: this.props.layoutKey }); - } const layout = getLayoutConfig(layoutKey, layoutName); const model = FlexLayout.Model.fromJson(layout); return model; @@ -138,6 +144,9 @@ class FlexLayoutWrapper extends React.PureComponent { this.updateToModelStateAndAdjustIt(model); this.setState({ model }); setTimeout(this.onLayoutChange, 1); + if (this.props.layoutName !== DEFAULT_LAYOUT_NAME) { + sendAnalyticsEvent("load_custom_layout", { viewMode: this.props.layoutKey }); + } } attachKeyboardShortcuts() { @@ -313,7 +322,6 @@ class FlexLayoutWrapper extends React.PureComponent { } onLayoutChange = () => { - sendAnalyticsEvent("change_tracing_layout", { viewMode: this.props.layoutKey }); const currentLayoutModel = _.cloneDeep(this.state.model.toJson()); // Workaround so that onLayoutChange is called after the update of flexlayout. // Calling the method without a timeout results in incorrect calculation of the viewport positions for the rendering. @@ -352,6 +360,9 @@ class FlexLayoutWrapper extends React.PureComponent { this.props.setActiveViewport(OrthoViews[toggledViewportId]); } } + if (!ignoredLayoutChangesByAnalytics.includes(type)) { + sendAnalyticsEvent("change_tracing_layout", { viewMode: this.props.layoutKey }); + } return action; }; diff --git a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js index d5ab449ba6b..e8b16f95eea 100644 --- a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js +++ b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js @@ -18,7 +18,7 @@ import type { OxalisState, AnnotationType, TraceOrViewCommand } from "oxalis/sto import { RenderToPortal } from "oxalis/view/layouting/portal_utils"; import { updateUserSettingAction } from "oxalis/model/actions/settings_actions"; import ActionBarView from "oxalis/view/action_bar_view"; -import ContextMenu from "oxalis/view/context_menu"; +import ContextMenuContainer from "oxalis/view/context_menu"; import ButtonComponent from "oxalis/view/components/button_component"; import NmlUploadZoneContainer from "oxalis/view/nml_upload_zone_container"; import OxalisController from "oxalis/controller"; @@ -271,16 +271,18 @@ class TracingLayoutView extends React.PureComponent { return ( - {contextMenuPosition != null && contextMenuViewport != null ? ( - - ) : null} + )} + { 500, ); - getClippingValues = async (layerName: string, thresholdRatio: number = 0.05) => { - const { elementClass } = getLayerByName(Store.getState().dataset, layerName); - const [TypedArrayClass] = getConstructorForElementClass(elementClass); - - const [cuboidXY, cuboidXZ, cuboidYZ] = await Promise.all([ - api.data.getViewportData(OrthoViews.PLANE_XY, layerName), - api.data.getViewportData(OrthoViews.PLANE_XZ, layerName), - api.data.getViewportData(OrthoViews.PLANE_YZ, layerName), - ]); - const dataForAllViewports = new TypedArrayClass( - cuboidXY.length + cuboidXZ.length + cuboidYZ.length, - ); - - dataForAllViewports.set(cuboidXY); - dataForAllViewports.set(cuboidXZ, cuboidXY.length); - dataForAllViewports.set(cuboidYZ, cuboidXY.length + cuboidXZ.length); - - const localHist = new Map(); - for (let i = 0; i < dataForAllViewports.length; i++) { - if (dataForAllViewports[i] !== 0) { - const value = localHist.get(dataForAllViewports[i]); - localHist.set(dataForAllViewports[i], value != null ? value + 1 : 1); - } - } - - const sortedHistKeys = Array.from(localHist.keys()).sort((a, b) => a - b); - const accumulator = new Map(); - let area = 0; - for (const key of sortedHistKeys) { - const value = localHist.get(key); - area += value != null ? value : 0; - accumulator.set(key, area); - } - const thresholdValue = (thresholdRatio * area) / 2.0; - - let lowerClip = -1; - for (const key of sortedHistKeys) { - const value = accumulator.get(key); - if (value != null && value >= thresholdValue) { - lowerClip = key; - break; - } - } - let upperClip = -1; - for (const key of sortedHistKeys.reverse()) { - const value = accumulator.get(key); - if (value != null && value < area - thresholdValue) { - upperClip = key; - break; - } - } - return [lowerClip, upperClip]; - }; - - clipHistogram = async (isInEditMode: boolean, layerName: string) => { - const [lowerClip, upperClip] = await this.getClippingValues(layerName); - if (lowerClip === -1 || upperClip === -1) { - Toast.warning( - "The histogram could not be clipped, because the data did not contain any brightness values greater than 0.", - ); - return; - } - if (!isInEditMode) { - this.onThresholdChange([lowerClip, upperClip]); - } else { - this.onThresholdChange([lowerClip, upperClip]); - this.setState({ currentMin: lowerClip, currentMax: upperClip }); - this.props.onChangeLayer(layerName, "min", lowerClip); - this.props.onChangeLayer(layerName, "max", upperClip); - } - }; - render() { const { intensityRangeMin, @@ -280,31 +202,8 @@ class Histogram extends React.PureComponent { `Enter the ${minimumOrMaximum} possible value for layer ${layerName}. Scientific (e.g. 9e+10) notation is supported.`; const minMaxInputStyle = { width: "100%" }; - const editModeAddendum = isInEditMode - ? "In Edit Mode, the histogram's range will be adjusted, too." - : ""; - const tooltipText = `Automatically clip the histogram to enhance contrast. ${editModeAddendum}`; return ( -
- - this.clipHistogram(isInEditMode, layerName)} - /> - -
{ this.canvasRef = ref; diff --git a/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.js b/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.js index 396733b4d67..ef8570bd124 100644 --- a/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.js +++ b/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.js @@ -10,6 +10,7 @@ import { StopOutlined, WarningOutlined, PlusOutlined, + VerticalAlignMiddleOutlined, } from "@ant-design/icons"; import { connect } from "react-redux"; import React from "react"; @@ -61,6 +62,7 @@ import { updateUserSettingAction, updateDatasetSettingAction, updateLayerSettingAction, + clipHistogramAction, } from "oxalis/model/actions/settings_actions"; import { userSettings } from "types/schemas/user_settings.schema"; import Constants, { type Vector3, type ControlMode, ControlModeEnum } from "oxalis/constants"; @@ -98,6 +100,7 @@ type DatasetSettingsProps = {| propertyName: $Keys, value: any, ) => void, + onClipHistogram: (layerName: string, shouldAdjustClipRange: boolean) => void, histogramData: HistogramDataForAllLayers, onChangeRadius: (value: number) => void, onChangeShowSkeletons: boolean => void, @@ -201,7 +204,7 @@ class DatasetSettings extends React.PureComponent { style={{ position: "absolute", top: 4, - right: 38, + right: 36, cursor: "pointer", }} /> @@ -237,7 +240,7 @@ class DatasetSettings extends React.PureComponent { style={{ position: "absolute", top: 4, - right: 38, + right: 36, cursor: "pointer", color: isInEditMode ? "var(--ant-primary)" : null, }} @@ -246,6 +249,27 @@ class DatasetSettings extends React.PureComponent { ); }; + getClipButton = (layerName: string, isInEditMode: boolean) => { + const editModeAddendum = isInEditMode + ? "In Edit Mode, the histogram's range will be adjusted, too." + : ""; + const tooltipText = `Automatically clip the histogram to enhance contrast. ${editModeAddendum}`; + return ( + + this.props.onClipHistogram(layerName, isInEditMode)} + /> + + ); + }; + setVisibilityForAllLayers = (isVisible: boolean) => { const { layers } = this.props.datasetConfiguration; Object.keys(layers).forEach(otherLayerName => @@ -474,6 +498,7 @@ class DatasetSettings extends React.PureComponent { ) : null} {isColorLayer ? null : this.getOptionalDownsampleVolumeIcon(maybeVolumeTracing)} + {hasHistogram && !isDisabled ? this.getClipButton(layerName, isInEditMode) : null} {hasHistogram && !isDisabled ? this.getEditMinMaxButton(layerName, isInEditMode) : null} {this.getFindDataButton(layerName, isDisabled, isColorLayer, maybeVolumeTracing)} {this.getReloadDataButton(layerName)} @@ -895,6 +920,9 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ onChangeLayer(layerName, propertyName, value) { dispatch(updateLayerSettingAction(layerName, propertyName, value)); }, + onClipHistogram(layerName, shouldAdjustClipRange) { + dispatch(clipHistogramAction(layerName, shouldAdjustClipRange)); + }, onChangeRadius(radius: number) { dispatch(setNodeRadiusAction(radius)); }, diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/dataset_info_tab_view.js b/frontend/javascripts/oxalis/view/right-border-tabs/dataset_info_tab_view.js index 6ca10936ddc..5ed798c423d 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/dataset_info_tab_view.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/dataset_info_tab_view.js @@ -32,7 +32,15 @@ import EditableTextLabel from "oxalis/view/components/editable_text_label"; import Model from "oxalis/model"; import features from "features"; import Store, { type OxalisState, type Task, type Tracing } from "oxalis/store"; -import NucleiInferralModal from "oxalis/view/right-border-tabs/nuclei_inferral_modal"; +import { + NucleiInferralModal, + NeuronInferralModal, +} from "oxalis/view/right-border-tabs/starting_job_modals"; + +const StartableJobsEnum = { + NUCLEI_INFERRAL: "nuclei inferral", + NEURON_INFERRAL: "neuron inferral", +}; type StateProps = {| tracing: Tracing, @@ -49,7 +57,7 @@ type DispatchProps = {| type Props = {| ...StateProps, ...DispatchProps |}; type State = { - showNucleiInferralModal: boolean, + showJobsDetailsModal: ?$Values, }; const shortcuts = [ @@ -132,9 +140,17 @@ export function convertPixelsToNm( return lengthInPixel * zoomValue * getBaseVoxel(dataset.dataSource.scale); } +export function convertNmToPixels( + lengthInNm: number, + zoomValue: number, + dataset: APIDataset, +): number { + return lengthInNm / (zoomValue * getBaseVoxel(dataset.dataSource.scale)); +} + class DatasetInfoTabView extends React.PureComponent { state = { - showNucleiInferralModal: false, + showJobsDetailsModal: null, }; setAnnotationName = (newName: string) => { @@ -200,7 +216,8 @@ class DatasetInfoTabView extends React.PureComponent { } getProcessingJobsMenu = () => { - if (!this.props.dataset.jobsEnabled) { + const { dataset } = this.props; + if (!dataset.jobsEnabled) { return ( @@ -216,21 +233,34 @@ class DatasetInfoTabView extends React.PureComponent { ); } - const overlay = ( - - this.setState({ showNucleiInferralModal: true })}> - - Start Nuclei Inferral + const jobMenuItems = [ + this.setState({ showJobsDetailsModal: StartableJobsEnum.NUCLEI_INFERRAL })} + > + + Start Nuclei Inferral + + , + ]; + if (this.props.activeUser != null && this.props.activeUser.isSuperUser) { + jobMenuItems.push( + this.setState({ showJobsDetailsModal: StartableJobsEnum.NEURON_INFERRAL })} + > + + Start Neuron Inferral - - - ); + , + ); + } return ( - + {jobMenuItems}
} overlayStyle={{ minWidth: "unset" }}>
{this.getTracingStatistics()}
diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/nuclei_inferral_modal.js b/frontend/javascripts/oxalis/view/right-border-tabs/nuclei_inferral_modal.js deleted file mode 100644 index a4d41e36226..00000000000 --- a/frontend/javascripts/oxalis/view/right-border-tabs/nuclei_inferral_modal.js +++ /dev/null @@ -1,120 +0,0 @@ -// @flow -import React, { useEffect, useState } from "react"; -import { type APIDataset } from "types/api_flow_types"; -import { Modal, Select, Button } from "antd"; -import { startNucleiInferralJob } from "admin/admin_rest_api"; -import { getColorLayers } from "oxalis/model/accessors/dataset_accessor"; -import Toast from "libs/toast"; -import { Unicode } from "oxalis/constants"; - -const { ThinSpace } = Unicode; - -type Props = { - dataset: APIDataset, - handleClose: () => void, -}; - -export default function NucleiInferralModal(props: Props) { - const { dataset, handleClose } = props; - const [selectedColorLayerName, setSelectedColorLayerName] = useState(null); - const colorLayerNames = getColorLayers(dataset).map(layer => layer.name); - useEffect(() => { - if (colorLayerNames.length === 1) { - setSelectedColorLayerName(colorLayerNames[0]); - } - }); - if (colorLayerNames.length < 1) { - return null; - } - - const onChange = selectedLayerName => { - setSelectedColorLayerName(selectedLayerName); - }; - - const startJob = async () => { - if (selectedColorLayerName == null) { - return; - } - try { - await startNucleiInferralJob( - dataset.owningOrganization, - dataset.name, - selectedColorLayerName, - ); - Toast.info( - <> - The nuclei inferral job has been started. You can look in the{" "} - - Processing Jobs - {" "} - view under Administration for details on the progress of this job. - , - ); - handleClose(); - } catch (error) { - console.error(error); - Toast.error( - "The nuclei inferral job could not be started. Please contact an administrator or look in the console for more details.", - ); - handleClose(); - } - }; - - return ( - -

- Start a job that automatically detects nuclei for this dataset. This job creates a copy of - this dataset once it has finished. The new dataset will contain the detected nuclei as a - segmentation layer.{" "} -

-

- - Note that this feature is still experimental. Nuclei detection currently works best with - EM data and a resolution of approximately 200{ThinSpace}nm per voxel. The inferral process - will automatically use the magnification that matches that resolution best. - -

-
-
- Nuclei inferral example -
-
- {colorLayerNames.length > 1 ? ( - -

- The detection approach uses a single color layer to predict the nuclei. Please select - the layer that should be used for detection. -

-
- -
-
-
- ) : null} -
- -
-
- ); -} diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/starting_job_modals.js b/frontend/javascripts/oxalis/view/right-border-tabs/starting_job_modals.js new file mode 100644 index 00000000000..69ff130cdd1 --- /dev/null +++ b/frontend/javascripts/oxalis/view/right-border-tabs/starting_job_modals.js @@ -0,0 +1,270 @@ +// @flow +import React, { useEffect, useState, type Node } from "react"; +import { type APIDataset, type APIJob } from "types/api_flow_types"; +import { Modal, Select, Button } from "antd"; +import { startNucleiInferralJob, startNeuronInferralJob } from "admin/admin_rest_api"; +import { useSelector } from "react-redux"; +import { getColorLayers } from "oxalis/model/accessors/dataset_accessor"; +import { getUserBoundingBoxesFromState } from "oxalis/model/accessors/tracing_accessor"; +import Toast from "libs/toast"; +import { type OxalisState, type UserBoundingBox } from "oxalis/store"; +import { Unicode, type Vector3 } from "oxalis/constants"; +import { capitalizeWords, computeArrayFromBoundingBox, rgbToHex } from "libs/utils"; + +const { ThinSpace } = Unicode; + +const jobNameToImagePath = { + "neuron inferral": "neuron_inferral_example.jpg", + "nuclei inferral": "nuclei_inferral_example.jpg", +}; + +type Props = { + handleClose: () => void, +}; +type StartingJobModalProps = { + ...Props, + dataset: APIDataset, + jobApiCall: (string, ?UserBoundingBox) => Promise, + jobName: string, + description: Node, + isBoundingBoxConfigurable?: boolean, +}; + +function StartingJobModal(props: StartingJobModalProps) { + const isBoundingBoxConfigurable = props.isBoundingBoxConfigurable || false; + const { dataset, handleClose, jobName, description, jobApiCall } = props; + const userBoundingBoxes = useSelector((state: OxalisState) => + getUserBoundingBoxesFromState(state), + ); + const [selectedColorLayerName, setSelectedColorLayerName] = useState(null); + const [selectedBoundingBox, setSelectedBoundingBox] = useState(null); + const colorLayerNames = getColorLayers(dataset).map(layer => layer.name); + useEffect(() => { + if (colorLayerNames.length === 1) { + setSelectedColorLayerName(colorLayerNames[0]); + } + }); + if (colorLayerNames.length < 1) { + return null; + } + const onChangeBoundingBox = (selectedBBoxId: number) => { + const selectedBBox = userBoundingBoxes.find(bbox => bbox.id === selectedBBoxId); + if (selectedBBox) { + setSelectedBoundingBox(selectedBBox); + } + }; + const startJob = async () => { + if (selectedColorLayerName == null) { + return; + } + try { + let apiJob; + if (isBoundingBoxConfigurable) { + apiJob = await jobApiCall(selectedColorLayerName, selectedBoundingBox); + } else { + apiJob = await jobApiCall(selectedColorLayerName); + } + if (!apiJob) { + return; + } + Toast.info( + <> + The {jobName} job has been started. You can look in the{" "} + + Processing Jobs + {" "} + view under Administration for details on the progress of this job. + , + ); + handleClose(); + } catch (error) { + console.error(error); + Toast.error( + `The ${jobName} job could not be started. Please contact an administrator or look in the console for more details.`, + ); + handleClose(); + } + }; + + const ColorLayerSelection = (): Node => + colorLayerNames.length > 1 ? ( + +

Please select the layer that should be used for the inferral.

+
+ +
+
+
+ ) : null; + + const renderUserBoundingBox = (bbox: ?UserBoundingBox) => { + if (!bbox) { + return null; + } + const upscaledColor = ((bbox.color.map(colorPart => colorPart * 255): any): Vector3); + const colorAsHexString = rgbToHex(upscaledColor); + return ( + <> +
+ {bbox.name} ({computeArrayFromBoundingBox(bbox.boundingBox).join(", ")}) + + ); + }; + const BoundingBoxSelection = (): Node => + isBoundingBoxConfigurable ? ( + +

+ Please select the bounding box for which the inferral should be computed. Note that large + bounding boxes can take very long. You can create a new bounding box for the desired + volume with the bounding box tool in the toolbar at the top. The created bounding boxes + will be listed below. +

+
+ +
+
+
+ ) : null; + + const hasUnselectedOptions = + selectedColorLayerName == null || (isBoundingBoxConfigurable && selectedBoundingBox == null); + + return ( + + {description} +
+
+ {`${jobName} +
+
+ + +
+ +
+
+ ); +} + +export function NucleiInferralModal({ handleClose }: Props) { + const dataset = useSelector((state: OxalisState) => state.dataset); + return ( + + startNucleiInferralJob(dataset.owningOrganization, dataset.name, colorLayerName) + } + description={ + <> +

+ Start a job that automatically detects nuclei for this dataset. This job creates a copy + of this dataset once it has finished. The new dataset will contain the detected nuclei + as a segmentation layer. +

+

+ + Note that this feature is still experimental. Nuclei detection currently works best + with EM data and a resolution of approximately 200{ThinSpace}nm per voxel. The + inferral process will automatically use the magnification that matches that resolution + best. + +

+ + } + /> + ); +} + +export function NeuronInferralModal({ handleClose }: Props) { + const dataset = useSelector((state: OxalisState) => state.dataset); + return ( + { + if (!boundingBox) { + return Promise.resolve(); + } + const bbox = computeArrayFromBoundingBox(boundingBox.boundingBox); + return startNeuronInferralJob( + dataset.owningOrganization, + dataset.name, + colorLayerName, + bbox, + ); + }} + description={ + <> +

+ Start a job that automatically detects the neurons for this dataset. This job creates a + copy of this dataset once it has finished. The new dataset will contain the new + segmentation which segments the neurons of the dataset. +

+

+ + Note that this feature is still experimental and can take a long time. Thus we suggest + to use a small bounding box and not the full dataset extent. The neuron detection + currently works best with EM data. The best resolution for the process will be chosen + automatically. + +

+ + } + /> + ); +} diff --git a/frontend/javascripts/oxalis/view/scalebar.js b/frontend/javascripts/oxalis/view/scalebar.js index cdac39d8b60..21350860864 100644 --- a/frontend/javascripts/oxalis/view/scalebar.js +++ b/frontend/javascripts/oxalis/view/scalebar.js @@ -22,8 +22,8 @@ type OwnProps = {| type StateProps = {| dataset: APIDataset, zoomValue: number, - widthInPixels: number, - heightInPixels: number, + viewportWidthInPixels: number, + viewportHeightInPixels: number, |}; type Props = {| @@ -31,25 +31,55 @@ type Props = {| ...StateProps, |}; -const scalebarWidthPercentage = 0.25; +const getBestScalebarAnchorInNm = (lengthInNm: number): number => { + const closestExponent = Math.floor(Math.log10(lengthInNm)); + const closestPowerOfTen = 10 ** closestExponent; + const mantissa = lengthInNm / closestPowerOfTen; -function Scalebar({ zoomValue, dataset, widthInPixels, heightInPixels }: Props) { - const widthInNm = convertPixelsToNm(widthInPixels, zoomValue, dataset); - const heightInNm = convertPixelsToNm(heightInPixels, zoomValue, dataset); - const formattedScalebarWidth = formatNumberToLength(widthInNm * scalebarWidthPercentage); + let bestAnchor = 1; + for (const anchor of [2, 5, 10]) { + if (Math.abs(anchor - mantissa) < Math.abs(bestAnchor - mantissa)) { + bestAnchor = anchor; + } + } + return bestAnchor * closestPowerOfTen; +}; + +// This factor describes how wide the scalebar would ideally be. +// However, this is only a rough guideline, as the actual width is changed +// so that round length values are represented. +const idealScalebarWidthFactor = 0.3; + +const maxScaleBarWidthFactor = 0.45; +const minWidthToFillScalebar = 130; + +function Scalebar({ zoomValue, dataset, viewportWidthInPixels, viewportHeightInPixels }: Props) { + const viewportWidthInNm = convertPixelsToNm(viewportWidthInPixels, zoomValue, dataset); + const viewportHeightInNm = convertPixelsToNm(viewportHeightInPixels, zoomValue, dataset); + const idealWidthInNm = viewportWidthInNm * idealScalebarWidthFactor; + const scalebarWidthInNm = getBestScalebarAnchorInNm(idealWidthInNm); + const scaleBarWidthFactor = Math.min( + scalebarWidthInNm / viewportWidthInNm, + maxScaleBarWidthFactor, + ); + + const tooltip = [ + formatNumberToLength(viewportWidthInNm), + ThinSpace, + MultiplicationSymbol, + ThinSpace, + formatNumberToLength(viewportHeightInNm), + ].join(""); + const collapseScalebar = viewportWidthInPixels < minWidthToFillScalebar; + const limitScalebar = scaleBarWidthFactor === maxScaleBarWidthFactor; + const padding = 4; return (
Viewport Size:
-
- {formatNumberToLength(widthInNm)} - {ThinSpace} - {MultiplicationSymbol} - {ThinSpace} - {formatNumberToLength(heightInNm)}{" "} -
+
{tooltip}
} > @@ -58,10 +88,11 @@ function Scalebar({ zoomValue, dataset, widthInPixels, heightInPixels }: Props) position: "absolute", bottom: "1%", right: "1%", - // The scalebar should have a width of 25% from the actual viewport (without the borders) - width: `calc(25% - ${Math.round( - ((2 * OUTER_CSS_BORDER) / constants.VIEWPORT_WIDTH) * 100, - )}%)`, + width: collapseScalebar + ? 16 + : `calc(${scaleBarWidthFactor * 100}% - ${Math.round( + ((2 * OUTER_CSS_BORDER) / constants.VIEWPORT_WIDTH) * 100, + )}% + ${2 * padding}px)`, height: 14, background: "rgba(0, 0, 0, .3)", color: "white", @@ -69,17 +100,17 @@ function Scalebar({ zoomValue, dataset, widthInPixels, heightInPixels }: Props) fontSize: 12, lineHeight: "14px", boxSizing: "content-box", - padding: 4, + padding, }} >
- {formattedScalebarWidth} + {collapseScalebar ? "i" : formatNumberToLength(scalebarWidthInNm)}
@@ -93,8 +124,8 @@ const mapStateToProps = (state: OxalisState, ownProps: OwnProps): StateProps => return { zoomValue, dataset: state.dataset, - widthInPixels: width, - heightInPixels: height, + viewportWidthInPixels: width, + viewportHeightInPixels: height, }; }; diff --git a/frontend/javascripts/router.js b/frontend/javascripts/router.js index f492ee46631..80aaffbb111 100644 --- a/frontend/javascripts/router.js +++ b/frontend/javascripts/router.js @@ -76,8 +76,8 @@ browserHistory.listen(location => { // The listener is called repeatedly for a single page change, don't send repeated pageviews if (lastPage !== newPage) { // Update the tracker state first, so that subsequent pageviews AND events use the correct page - window.ga("set", "page", newPage); - window.ga("send", "pageview"); + window.gtag("set", "page_path", newPage); + window.gtag("event", "page_view"); } } }); diff --git a/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js b/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js index a97c97e2031..d6ead71ebe4 100644 --- a/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js +++ b/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js @@ -108,7 +108,7 @@ export async function screenshotSandboxWithMappingLink( async function waitForMappingEnabled(page: Page) { let isMappingEnabled; while (!isMappingEnabled) { - await page.waitFor(5000); + await page.waitForTimeout(5000); isMappingEnabled = await page.evaluate( "webknossos.apiReady().then(async api => api.data.isMappingEnabled())", ); @@ -118,7 +118,7 @@ async function waitForMappingEnabled(page: Page) { async function waitForTracingViewLoad(page: Page) { let inputCatchers; while (inputCatchers == null || inputCatchers.length < 4) { - await page.waitFor(500); + await page.waitForTimeout(500); inputCatchers = await page.$(".inputcatcher"); } } @@ -129,7 +129,7 @@ async function waitForRenderingFinish(page: Page) { let changedPixels = Infinity; // If the screenshot of the page didn't change in the last x seconds, rendering should be finished while (currentShot == null || changedPixels > 0) { - await page.waitFor(10000); + await page.waitForTimeout(10000); currentShot = await page.screenshot({ fullPage: true }); if (lastShot != null) { changedPixels = pixelmatch(lastShot, currentShot, {}, 1920, 1080, { diff --git a/frontend/javascripts/test/sagas/save_saga.spec.js b/frontend/javascripts/test/sagas/save_saga.spec.js index 633c439f656..945dc778c36 100644 --- a/frontend/javascripts/test/sagas/save_saga.spec.js +++ b/frontend/javascripts/test/sagas/save_saga.spec.js @@ -103,7 +103,7 @@ test("SaveSaga should send update actions", t => { saga.next(); // select state expectValueDeepEqual(t, saga.next([]), take("PUSH_SAVE_QUEUE_TRANSACTION")); saga.next(); // race - saga.next(SaveActions.pushSaveQueueTransaction(updateActions)); + saga.next({ forcePush: SaveActions.saveNowAction() }); saga.next(); // select state expectValueDeepEqual(t, saga.next(saveQueue), call(sendRequestToServer, TRACING_TYPE, tracingId)); saga.next(); // select state @@ -197,7 +197,7 @@ test("SaveSaga should escalate on permanent client error update actions", t => { t.true(saga.next().done); }); -test("SaveSaga should send update actions right away", t => { +test("SaveSaga should send update actions right away and try to reach a state where all updates are saved", t => { const updateActions = [UpdateActions.createEdge(1, 0, 1), UpdateActions.createEdge(1, 1, 2)]; const saveQueue = createSaveQueueFromUpdateActions(updateActions, TIMESTAMP); @@ -207,13 +207,28 @@ test("SaveSaga should send update actions right away", t => { saga.next(); // select state expectValueDeepEqual(t, saga.next([]), take("PUSH_SAVE_QUEUE_TRANSACTION")); saga.next(); // race - saga.next(SaveActions.saveNowAction()); // put setSaveBusyAction + saga.next({ forcePush: SaveActions.saveNowAction() }); // put setSaveBusyAction saga.next(); // select state saga.next(saveQueue); // call sendRequestToServer saga.next(); // select state expectValueDeepEqual(t, saga.next([]), put(setSaveBusyAction(false, TRACING_TYPE))); }); +test("SaveSaga should not try to reach state with all actions being saved when saving is triggered by a timeout", t => { + const updateActions = [UpdateActions.createEdge(1, 0, 1), UpdateActions.createEdge(1, 1, 2)]; + const saveQueue = createSaveQueueFromUpdateActions(updateActions, TIMESTAMP); + + const saga = pushTracingTypeAsync(TRACING_TYPE, tracingId); + expectValueDeepEqual(t, saga.next(), take(INIT_ACTION)); + saga.next(); + saga.next(); // select state + expectValueDeepEqual(t, saga.next([]), take("PUSH_SAVE_QUEUE_TRANSACTION")); + saga.next(); // race + saga.next({ timeout: "a placeholder" }); // put setSaveBusyAction + saga.next(saveQueue); // call sendRequestToServer + expectValueDeepEqual(t, saga.next([]), put(setSaveBusyAction(false, TRACING_TYPE))); +}); + test("SaveSaga should remove the correct update actions", t => { const saveQueue = createSaveQueueFromUpdateActions( [ diff --git a/frontend/javascripts/test/screenshots/2017-05-31_mSEM_aniso-test.png b/frontend/javascripts/test/screenshots/2017-05-31_mSEM_aniso-test.png index cf7ce37d72d..c6f06556f93 100644 Binary files a/frontend/javascripts/test/screenshots/2017-05-31_mSEM_aniso-test.png and b/frontend/javascripts/test/screenshots/2017-05-31_mSEM_aniso-test.png differ diff --git a/frontend/javascripts/test/screenshots/2017-05-31_mSEM_scMS109_bk_100um_v01-aniso.png b/frontend/javascripts/test/screenshots/2017-05-31_mSEM_scMS109_bk_100um_v01-aniso.png index 48324d9e524..f5a62b6916b 100644 Binary files a/frontend/javascripts/test/screenshots/2017-05-31_mSEM_scMS109_bk_100um_v01-aniso.png and b/frontend/javascripts/test/screenshots/2017-05-31_mSEM_scMS109_bk_100um_v01-aniso.png differ diff --git a/frontend/javascripts/test/screenshots/Multi-Channel-Test.png b/frontend/javascripts/test/screenshots/Multi-Channel-Test.png index 5ca323dded0..8cd1e88adfe 100644 Binary files a/frontend/javascripts/test/screenshots/Multi-Channel-Test.png and b/frontend/javascripts/test/screenshots/Multi-Channel-Test.png differ diff --git a/frontend/javascripts/test/screenshots/ROI2017_wkw.png b/frontend/javascripts/test/screenshots/ROI2017_wkw.png index 18a366e1cd3..859346f90eb 100644 Binary files a/frontend/javascripts/test/screenshots/ROI2017_wkw.png and b/frontend/javascripts/test/screenshots/ROI2017_wkw.png differ diff --git a/frontend/javascripts/test/screenshots/ROI2017_wkw_fallback.png b/frontend/javascripts/test/screenshots/ROI2017_wkw_fallback.png index cd1ca2c931a..3e728dd8dac 100644 Binary files a/frontend/javascripts/test/screenshots/ROI2017_wkw_fallback.png and b/frontend/javascripts/test/screenshots/ROI2017_wkw_fallback.png differ diff --git a/frontend/javascripts/test/screenshots/ROI2017_wkw_with_mapping_astrocyte.png b/frontend/javascripts/test/screenshots/ROI2017_wkw_with_mapping_astrocyte.png index cb29964bbb9..8f7f20a8dc5 100644 Binary files a/frontend/javascripts/test/screenshots/ROI2017_wkw_with_mapping_astrocyte.png and b/frontend/javascripts/test/screenshots/ROI2017_wkw_with_mapping_astrocyte.png differ diff --git a/frontend/javascripts/test/screenshots/dsA_2.png b/frontend/javascripts/test/screenshots/dsA_2.png index 0572f046a01..504c8bcd9ec 100644 Binary files a/frontend/javascripts/test/screenshots/dsA_2.png and b/frontend/javascripts/test/screenshots/dsA_2.png differ diff --git a/frontend/javascripts/test/screenshots/float_test_dataset.png b/frontend/javascripts/test/screenshots/float_test_dataset.png index 8aa2d508ab5..195ba03a403 100644 Binary files a/frontend/javascripts/test/screenshots/float_test_dataset.png and b/frontend/javascripts/test/screenshots/float_test_dataset.png differ diff --git a/frontend/javascripts/test/screenshots/test-agglomerate-file_with_mapping_link.png b/frontend/javascripts/test/screenshots/test-agglomerate-file_with_mapping_link.png index 0e2883c0bf1..61424cdab3f 100644 Binary files a/frontend/javascripts/test/screenshots/test-agglomerate-file_with_mapping_link.png and b/frontend/javascripts/test/screenshots/test-agglomerate-file_with_mapping_link.png differ diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/misc.e2e.js.md b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/misc.e2e.js.md index 779229f7e54..bc51beeab57 100644 --- a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/misc.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/misc.e2e.js.md @@ -40,6 +40,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460379469053, lastName: 'BoyA', lastTaskTypeId: null, diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/misc.e2e.js.snap b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/misc.e2e.js.snap index 9a9479a3349..802e2c0494c 100644 Binary files a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/misc.e2e.js.snap and b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/misc.e2e.js.snap differ diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/users.e2e.js.md b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/users.e2e.js.md index befdb1ae2bb..e89ff85087a 100644 --- a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/users.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/users.e2e.js.md @@ -20,6 +20,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460379469053, lastName: 'BoyA', lastTaskTypeId: null, @@ -55,6 +56,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460465869053, lastName: 'BoyB', lastTaskTypeId: null, @@ -80,6 +82,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: false, isEditable: true, + isSuperUser: true, lastActivity: 1460552269053, lastName: 'BoyC', lastTaskTypeId: null, @@ -105,6 +108,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: false, isEditable: true, + isSuperUser: true, lastActivity: 1460638669053, lastName: 'BoyD', lastTaskTypeId: null, @@ -137,6 +141,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460379469053, lastName: 'BoyA', lastTaskTypeId: null, @@ -179,6 +184,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460379469053, lastName: 'BoyA', lastTaskTypeId: null, @@ -214,6 +220,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460465869053, lastName: 'BoyB', lastTaskTypeId: null, @@ -239,6 +246,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: false, isEditable: true, + isSuperUser: true, lastActivity: 1460552269053, lastName: 'BoyC', lastTaskTypeId: null, @@ -264,6 +272,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: false, isEditable: true, + isSuperUser: true, lastActivity: 1460638669053, lastName: 'BoyD', lastTaskTypeId: null, @@ -295,6 +304,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460379469053, lastName: 'BoyA', lastTaskTypeId: null, @@ -335,6 +345,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460379469053, lastName: 'BoyA', lastTaskTypeId: null, @@ -375,6 +386,7 @@ Generated by [AVA](https://avajs.dev). isAnonymous: false, isDatasetManager: true, isEditable: true, + isSuperUser: true, lastActivity: 1460379469053, lastName: 'BoyA', lastTaskTypeId: null, diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/users.e2e.js.snap b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/users.e2e.js.snap index 98dd52c7604..96c51f0c07b 100644 Binary files a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/users.e2e.js.snap and b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/users.e2e.js.snap differ diff --git a/frontend/javascripts/types/api_flow_types.js b/frontend/javascripts/types/api_flow_types.js index 42ca344c2f4..4d74af860f9 100644 --- a/frontend/javascripts/types/api_flow_types.js +++ b/frontend/javascripts/types/api_flow_types.js @@ -216,6 +216,7 @@ export type APIUser = APIUserBase & { +experiences: ExperienceMap, +isAdmin: boolean, +isDatasetManager: boolean, + +isSuperUser: boolean, +isActive: boolean, +isEditable: boolean, +lastActivity: number, diff --git a/frontend/stylesheets/trace_view/_right_menu.less b/frontend/stylesheets/trace_view/_right_menu.less index 3108fd308ae..e4c70613ff2 100644 --- a/frontend/stylesheets/trace_view/_right_menu.less +++ b/frontend/stylesheets/trace_view/_right_menu.less @@ -164,3 +164,12 @@ .margin-bottom { margin-bottom: 10px; } + +.color-display-wrapper { + display: inline-block; + width: 16px; + height: 16px; + border-radius: 3px; + box-shadow: 0px 0px 3px #cacaca; + vertical-align: middle; +} diff --git a/frontend/stylesheets/trace_view/_tracing_view.less b/frontend/stylesheets/trace_view/_tracing_view.less index b062dc8c6e5..1497b97e3d8 100644 --- a/frontend/stylesheets/trace_view/_tracing_view.less +++ b/frontend/stylesheets/trace_view/_tracing_view.less @@ -139,20 +139,42 @@ color: @text-color; background: @component-background; box-shadow: @box-shadow-base; + .node-context-menu-item { - font-size: 12px; - height: 30px; + clear: both; + margin: 0; + padding: 5px 12px; + font-weight: normal; + font-size: 14px; + line-height: 22px; + white-space: nowrap; + } + .keyboard-key-icon-small { line-height: 30px; - padding: 0px 10px; } - .ant-menu { - border-right: none; + + // Antd is asked to render the dropdown menu + // into .node-context-menu. However, antd expects + // that div to be positioned at 0,0 and tries to + // position the menu within the div accordingly. + // However, we are already positioning the div + // at the desired spot. Therefore, the dropdown itself + // should be positioned statically. + .ant-dropdown { + position: static !important; + } + + > div { + // Fixes too wide context menu + width: unset !important; + // Ensures that the context menu is correctly trapped + // within its sticky container. + position: unset !important; } -} -.node-context-menu-sub { - font-size: 12px; - margin-left: -6px; + .dropdown-overlay-container-for-context-menu { + min-width: unset !important; + } } #version-restore-sider { diff --git a/package.json b/package.json index 48a8a1b95a1..81241e00219 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.9.1", "eslint": "^5.7.0", + "eslint_d": "^11.1.1", "eslint-config-airbnb": "^17.1.0", "eslint-config-prettier": "^3.1.0", "eslint-import-resolver-webpack": "^0.13.2", @@ -66,7 +67,7 @@ "pngjs": "^3.3.3", "prettier": "1.16.4", "proto-loader6": "^0.4.0", - "puppeteer": "^1.13.0", + "puppeteer": "^11.0.0", "randomstring": "^1.1.5", "react-test-renderer": "^16.8.0", "redux-mock-store": "^1.2.2", @@ -105,6 +106,7 @@ "flow": "node_modules/.bin/flow", "flow-check": "node_modules/.bin/flow check", "lint": "node_modules/.bin/eslint --cache --quiet frontend/javascripts/ tools/", + "lintd": "node_modules/.bin/eslint_d --cache --quiet frontend/javascripts/ tools/ # lintd uses a daemon for better speed (especially useful for hooking up to editors)", "lint-no-cache": "node_modules/.bin/eslint --quiet frontend/javascripts/ tools/", "pretty": "node_modules/.bin/prettier --write --config .prettierrc \"frontend/javascripts/**/*.js\" \"tools/**/*.js\"", "pretty-backend": "sbt \";scalafmt; util/scalafmt; webknossosTracingstore/scalafmt; webknossosDatastore/scalafmt\"", diff --git a/public/images/neuron_inferral_example.jpg b/public/images/neuron_inferral_example.jpg new file mode 100644 index 00000000000..b07d31137d7 Binary files /dev/null and b/public/images/neuron_inferral_example.jpg differ diff --git a/test/backend/Dummies.scala b/test/backend/Dummies.scala index a87090b6d94..666066018d7 100644 --- a/test/backend/Dummies.scala +++ b/test/backend/Dummies.scala @@ -3,20 +3,20 @@ package backend import com.scalableminds.webknossos.datastore.SkeletonTracing._ import com.scalableminds.webknossos.datastore.VolumeTracing.{Segment, VolumeTracing} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing.ElementClass -import com.scalableminds.webknossos.datastore.geometry.{BoundingBox, Color, Point3D, Vector3D} +import com.scalableminds.webknossos.datastore.geometry.{BoundingBoxProto, ColorProto, Vec3DoubleProto, Vec3IntProto} object Dummies { val timestamp = 123456789 val timestampLong = 123456789L def createDummyNode(id: Int): Node = - Node(id, Point3D(id, id + 1, id + 2), Vector3D(id, id + 1, id + 2), id, 1, 10, 8, id % 2 == 0, timestamp) + Node(id, Vec3IntProto(id, id + 1, id + 2), Vec3DoubleProto(id, id + 1, id + 2), id, 1, 10, 8, id % 2 == 0, timestamp) val tree1: Tree = Tree( 1, Seq(createDummyNode(0), createDummyNode(1), createDummyNode(2), createDummyNode(7)), Seq(Edge(0, 1), Edge(2, 1), Edge(1, 7)), - Some(Color(23, 23, 23, 1)), + Some(ColorProto(23, 23, 23, 1)), Seq(BranchPoint(1, 0), BranchPoint(7, 0)), Seq(Comment(0, "comment")), "TestTree-1", @@ -29,7 +29,7 @@ object Dummies { 2, Seq(createDummyNode(4), createDummyNode(5), createDummyNode(6)), Seq(Edge(4, 5), Edge(5, 6)), - Some(Color(30, 30, 30, 1)), + Some(ColorProto(30, 30, 30, 1)), Seq[BranchPoint](), Seq[Comment](), "TestTree-2", @@ -46,8 +46,8 @@ object Dummies { timestamp, None, Some(1), - Point3D(1, 1, 1), - Vector3D(1.0, 1.0, 1.0), + Vec3IntProto(1, 1, 1), + Vec3DoubleProto(1.0, 1.0, 1.0), 1.0, 0, None, @@ -70,8 +70,8 @@ object Dummies { timestamp, None, None, - Point3D(1, 1, 1), - Vector3D(1.0, 1.0, 1.0), + Vec3IntProto(1, 1, 1), + Vec3DoubleProto(1.0, 1.0, 1.0), 1.0, 0, None, @@ -79,16 +79,16 @@ object Dummies { val volumeTracing: VolumeTracing = VolumeTracing( None, - BoundingBox(Point3D(0,0,0), 10, 10, 10), + BoundingBoxProto(Vec3IntProto(0,0,0), 10, 10, 10), timestamp, "dummy_dataset", - Point3D(1, 1, 1), - Vector3D(1.0, 1.0, 1.0), + Vec3IntProto(1, 1, 1), + Vec3DoubleProto(1.0, 1.0, 1.0), ElementClass.uint16, None, 5, 0, 1.0, - segments = Seq(Segment(5, Some(Point3D(7,7,7)))) + segments = Seq(Segment(5, Some(Vec3IntProto(7,7,7)))) ) } diff --git a/test/backend/NMLUnitTestSuite.scala b/test/backend/NMLUnitTestSuite.scala index beaf3c96a3e..3bd7c82a9b7 100644 --- a/test/backend/NMLUnitTestSuite.scala +++ b/test/backend/NMLUnitTestSuite.scala @@ -3,9 +3,8 @@ package backend import java.io.ByteArrayInputStream import com.scalableminds.webknossos.datastore.SkeletonTracing._ -import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing -import models.annotation.FetchedAnnotationLayer import models.annotation.nml.{NmlParser, NmlWriter} +import models.annotation.{FetchedAnnotationLayer, UploadedVolumeLayer} import net.liftweb.common.{Box, Full} import org.scalatestplus.play.PlaySpec import play.api.i18n.{DefaultMessagesApi, Messages, MessagesProvider} @@ -23,7 +22,7 @@ class NMLUnitTestSuite extends PlaySpec { } def writeAndParseTracing(skeletonTracing: SkeletonTracing) - : Box[(Option[SkeletonTracing], Option[(VolumeTracing, String)], String)] = { + : Box[(Option[SkeletonTracing], List[UploadedVolumeLayer], String)] = { val annotationLayers = List(FetchedAnnotationLayer("dummySkeletonTracingId", None, Left(skeletonTracing), None)) val nmlEnumarator = new NmlWriter().toNmlStream(annotationLayers, None, None, None, "testOrganization", None, None) @@ -33,7 +32,7 @@ class NMLUnitTestSuite extends PlaySpec { } def isParseSuccessful( - parsedTracing: Box[(Option[SkeletonTracing], Option[(VolumeTracing, String)], String)]): Boolean = + parsedTracing: Box[(Option[SkeletonTracing], List[UploadedVolumeLayer], String)]): Boolean = parsedTracing match { case Full(tuple) => tuple match { diff --git a/test/backend/SkeletonUpdateActionsUnitTestSuite.scala b/test/backend/SkeletonUpdateActionsUnitTestSuite.scala index 144a8513897..73dfe768f70 100644 --- a/test/backend/SkeletonUpdateActionsUnitTestSuite.scala +++ b/test/backend/SkeletonUpdateActionsUnitTestSuite.scala @@ -1,6 +1,6 @@ package backend -import com.scalableminds.util.geometry.{Point3D, Vector3D} +import com.scalableminds.util.geometry.{Vec3Int, Vec3Double} import com.scalableminds.webknossos.datastore.SkeletonTracing._ import com.scalableminds.webknossos.tracingstore.tracings._ import com.scalableminds.webknossos.tracingstore.tracings.skeleton.updating._ @@ -143,8 +143,8 @@ class SkeletonUpdateActionsUnitTestSuite extends PlaySpec { val newNode = Dummies.createDummyNode(100) val createNodeSkeletonAction = new CreateNodeSkeletonAction( newNode.id, - Point3D(newNode.position.x, newNode.position.y, newNode.position.z), - Option(Vector3D(newNode.rotation.x, newNode.rotation.y, newNode.rotation.z)), + Vec3Int(newNode.position.x, newNode.position.y, newNode.position.z), + Option(Vec3Double(newNode.rotation.x, newNode.rotation.y, newNode.rotation.z)), Option(newNode.radius), Option(newNode.viewport), Option(newNode.resolution), @@ -167,8 +167,8 @@ class SkeletonUpdateActionsUnitTestSuite extends PlaySpec { val newNode = Dummies.createDummyNode(1) val updateNodeSkeletonAction = new UpdateNodeSkeletonAction( newNode.id, - Point3D(newNode.position.x, newNode.position.y, newNode.position.z), - Option(Vector3D(newNode.rotation.x, newNode.rotation.y, newNode.rotation.z)), + Vec3Int(newNode.position.x, newNode.position.y, newNode.position.z), + Option(Vec3Double(newNode.rotation.x, newNode.rotation.y, newNode.rotation.z)), Option(newNode.radius), Option(newNode.viewport), Option(newNode.resolution), @@ -191,8 +191,8 @@ class SkeletonUpdateActionsUnitTestSuite extends PlaySpec { val newNode = Dummies.createDummyNode(100) val createNodeSkeletonAction = new CreateNodeSkeletonAction( newNode.id, - Point3D(newNode.position.x, newNode.position.y, newNode.position.z), - Option(Vector3D(newNode.rotation.x, newNode.rotation.y, newNode.rotation.z)), + Vec3Int(newNode.position.x, newNode.position.y, newNode.position.z), + Option(Vec3Double(newNode.rotation.x, newNode.rotation.y, newNode.rotation.z)), Option(newNode.radius), Option(newNode.viewport), Option(newNode.resolution), @@ -236,8 +236,8 @@ class SkeletonUpdateActionsUnitTestSuite extends PlaySpec { "UpdateTracingSkeletonAction" should { "update a top level tree group" in { val activeNode = Some(1) - val editPosition = Point3D(11, 12, 13) - val editRotation = Vector3D(21, 22, 23) + val editPosition = Vec3Int(11, 12, 13) + val editRotation = Vec3Double(21, 22, 23) val zoomLevel = 99 val userBoundingBox = None val updateTreeGroupsSkeletonAction = new UpdateTracingSkeletonAction( diff --git a/test/backend/VolumeUpdateActionsUnitTestSuite.scala b/test/backend/VolumeUpdateActionsUnitTestSuite.scala index f58d9b9f5d4..cee77fc3efa 100644 --- a/test/backend/VolumeUpdateActionsUnitTestSuite.scala +++ b/test/backend/VolumeUpdateActionsUnitTestSuite.scala @@ -1,6 +1,6 @@ package backend -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits import com.scalableminds.webknossos.tracingstore.tracings.volume.{ApplyableVolumeAction, CreateSegmentVolumeAction, DeleteSegmentVolumeAction, UpdateSegmentVolumeAction} @@ -15,7 +15,7 @@ class VolumeUpdateActionsUnitTestSuite extends PlaySpec with ProtoGeometryImplic "add the specified segment" in { val createSegmentAction = CreateSegmentVolumeAction( id = 1000, - anchorPosition = Some(Point3D(5,5,5)), + anchorPosition = Some(Vec3Int(5,5,5)), name = Some("aSegment"), creationTime = Some(Dummies.timestampLong) ) @@ -45,7 +45,7 @@ class VolumeUpdateActionsUnitTestSuite extends PlaySpec with ProtoGeometryImplic "update the specified segment" in { val updateSegmentAction = UpdateSegmentVolumeAction( id = 5, - anchorPosition = Some(Point3D(8,8,8)), + anchorPosition = Some(Vec3Int(8,8,8)), name = Some("aRenamedSegment"), creationTime = Some(Dummies.timestampLong) ) @@ -55,7 +55,7 @@ class VolumeUpdateActionsUnitTestSuite extends PlaySpec with ProtoGeometryImplic val segment = result.segments.find(_.segmentId == updateSegmentAction.id).get assert(segment.segmentId == updateSegmentAction.id) - assert(segment.anchorPosition.contains(point3DToProto(Point3D(8,8,8)))) + assert(segment.anchorPosition.contains(vec3IntToProto(Vec3Int(8,8,8)))) assert(segment.name.contains("aRenamedSegment")) assert(segment.creationTime.contains(Dummies.timestampLong)) } diff --git a/tools/proxy/yarn.lock b/tools/proxy/yarn.lock index 6abdf2c0c14..bc091365546 100644 --- a/tools/proxy/yarn.lock +++ b/tools/proxy/yarn.lock @@ -163,9 +163,9 @@ finalhandler@1.1.1: unpipe "~1.0.0" follow-redirects@^1.0.0: - version "1.14.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" - integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== forwarded@~0.1.2: version "0.1.2" diff --git a/util/src/main/scala/com/scalableminds/util/geometry/BoundingBox.scala b/util/src/main/scala/com/scalableminds/util/geometry/BoundingBox.scala index 294d45df095..9a86ee22bbd 100644 --- a/util/src/main/scala/com/scalableminds/util/geometry/BoundingBox.scala +++ b/util/src/main/scala/com/scalableminds/util/geometry/BoundingBox.scala @@ -2,9 +2,9 @@ package com.scalableminds.util.geometry import net.liftweb.common.{Box, Empty, Full} -case class BoundingBox(topLeft: Point3D, width: Int, height: Int, depth: Int) { +case class BoundingBox(topLeft: Vec3Int, width: Int, height: Int, depth: Int) { - val bottomRight: Point3D = topLeft.move(width, height, depth) + val bottomRight: Vec3Int = topLeft.move(width, height, depth) def intersects(other: BoundingBox): Boolean = math.max(topLeft.x, other.topLeft.x) < math.min(bottomRight.x, other.bottomRight.x) && @@ -20,13 +20,13 @@ case class BoundingBox(topLeft: Point3D, width: Int, height: Int, depth: Int) { val h = math.max(other.bottomRight.y, bottomRight.y) - y val d = math.max(other.bottomRight.z, bottomRight.z) - z - BoundingBox(Point3D(x, y, z), w, h, d) + BoundingBox(Vec3Int(x, y, z), w, h, d) } def isEmpty: Boolean = width <= 0 || height <= 0 || depth <= 0 - def center: Point3D = + def center: Vec3Int = topLeft.move(bottomRight).scale(0.5f) def scale(s: Float): BoundingBox = @@ -38,8 +38,8 @@ case class BoundingBox(topLeft: Point3D, width: Int, height: Int, depth: Int) { def volume: Long = width.toLong * height.toLong * depth.toLong - def dimensions: Point3D = - Point3D(width, height, depth) + def dimensions: Vec3Int = + Vec3Int(width, height, depth) } @@ -50,7 +50,7 @@ object BoundingBox { private val formRx = "\\s*([0-9]+),\\s*([0-9]+),\\s*([0-9]+)\\s*,\\s*([0-9]+),\\s*([0-9]+),\\s*([0-9]+)\\s*".r def empty: BoundingBox = - BoundingBox(Point3D(0, 0, 0), 0, 0, 0) + BoundingBox(Vec3Int(0, 0, 0), 0, 0, 0) def createFrom(s: String): Box[BoundingBox] = s match { @@ -58,7 +58,7 @@ object BoundingBox { try { Full( BoundingBox( - Point3D(Integer.parseInt(minX), Integer.parseInt(minY), Integer.parseInt(minZ)), + Vec3Int(Integer.parseInt(minX), Integer.parseInt(minY), Integer.parseInt(minZ)), Integer.parseInt(width), Integer.parseInt(height), Integer.parseInt(depth) @@ -75,7 +75,7 @@ object BoundingBox { case head :: tail => tail.foldLeft(head)(_ combineWith _) case _ => - BoundingBox(Point3D(0, 0, 0), 0, 0, 0) + BoundingBox(Vec3Int(0, 0, 0), 0, 0, 0) } def createFrom(bbox: List[List[Int]]): Box[BoundingBox] = @@ -83,12 +83,12 @@ object BoundingBox { Empty else Full( - BoundingBox(Point3D(bbox(0)(0), bbox(1)(0), bbox(2)(0)), + BoundingBox(Vec3Int(bbox(0)(0), bbox(1)(0), bbox(2)(0)), bbox(0)(1) - bbox(0)(0), bbox(1)(1) - bbox(1)(0), bbox(2)(1) - bbox(2)(0))) - def createFrom(topLeft: Point3D, bottomRight: Point3D): Box[BoundingBox] = + def createFrom(topLeft: Vec3Int, bottomRight: Vec3Int): Box[BoundingBox] = if (topLeft <= bottomRight) Full(BoundingBox(topLeft, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y, bottomRight.z - topLeft.z)) else @@ -96,7 +96,7 @@ object BoundingBox { def fromSQL(ints: List[Int]): Option[BoundingBox] = if (ints.length == 6) - Some(BoundingBox(Point3D(ints(0), ints(1), ints(2)), ints(3), ints(4), ints(5))) + Some(BoundingBox(Vec3Int(ints(0), ints(1), ints(2)), ints(3), ints(4), ints(5))) else None diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Figure.scala b/util/src/main/scala/com/scalableminds/util/geometry/Figure.scala deleted file mode 100644 index 48f6b2bfb63..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/Figure.scala +++ /dev/null @@ -1,78 +0,0 @@ -package com.scalableminds.util.geometry - -import com.scalableminds.util.tools.ExtendedTypes._ -import com.scalableminds.util.tools.Math._ - -import scala.collection.mutable.{ArrayBuffer, ArrayBuilder} -import scala.math._ - -abstract class Figure - -case class ConvexFigure(polygons: Seq[Polygon]) extends Figure { - - def isInside(point: (Double, Double, Double), polygonOfPoint: Polygon = null) = - !polygons.exists( - polygon => - polygon != polygonOfPoint && - polygon.normalVector ° point - polygon.d > EPSILON) - - def calculateInnerPoints(): Seq[Tuple3[Int, Int, Int]] = { - val vertices = this.polygons.flatMap(_.vertices) - - val maxVector = - vertices.foldLeft(vertices(0))((b, e) => Vector3D(math.max(b.x, e.x), math.max(b.y, e.y), math.max(b.z, e.z))) - - val minVector = - vertices.foldLeft(vertices(0))((b, e) => Vector3D(math.min(b.x, e.x), math.min(b.y, e.y), math.min(b.z, e.z))) - - val innerPoints = ArrayBuilder.make[(Int, Int, Int)]() - var zRangeBoundaries = ArrayBuffer[Int]() - - val directionalVector = new Vector3D(0, 0, 1) - val polygonsAndDivisors = - for { - polygon <- this.polygons - divisor = directionalVector ° polygon.normalVector - if !divisor.isNearZero - } yield (polygon, divisor) - - val max_x = maxVector.x.patchAbsoluteValue.toInt - val max_y = maxVector.y.patchAbsoluteValue.toInt - - val min_x = max(minVector.x.patchAbsoluteValue.toInt, 0) - val min_y = max(minVector.y.patchAbsoluteValue.toInt, 0) - - for { - x <- min_x to max_x - y <- min_y to max_y - } { - zRangeBoundaries = ArrayBuffer[Int]() - val rayPositionVector = new Vector3D(x, y, 0) - - for ((polygon, divisor) <- polygonsAndDivisors) { - val zBoundary = - (polygon.d - (rayPositionVector ° polygon.normalVector)) / divisor - if (this.isInside((x.toDouble, y.toDouble, zBoundary), polygon)) { - zRangeBoundaries.append(zBoundary.patchAbsoluteValue.toInt) - } - } - - if (!zRangeBoundaries.isEmpty) { - var lowerBoundary = zRangeBoundaries.min - val upperBoundary = zRangeBoundaries.max - - if (upperBoundary >= 0) { - lowerBoundary = max(lowerBoundary, 0) - - innerPoints ++= - (for (z <- lowerBoundary to upperBoundary) yield ((x, y, z))) - } - } - - } - innerPoints.result - } - - override def toString() = - polygons.toString -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/NGonalFrustum.scala b/util/src/main/scala/com/scalableminds/util/geometry/NGonalFrustum.scala deleted file mode 100644 index 05de164b769..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/NGonalFrustum.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.scalableminds.util.geometry - -/** - * Represents a regular n-gonal frustum. - */ -class NGonalFrustum(numberOfVertices: Int, height: Int, radiusBase: Int, radiusTop: Int) { - val polygons = { - // create the polygon which represents the base of the frustrum - val basePolygon = new RegularPolygon(numberOfVertices, radiusBase) - // top of the frustum - val topPolygon = new RegularPolygon(numberOfVertices, radiusTop) - // list of all polygons the frustum contains, top polygon will be the last in the list - // extend all vectors with a 3rd dimension - var polygons = new Polygon(topPolygon.to3D(height, 2).reverse) :: Nil // top gets reveresed to make it counter clockwise - for (i <- 0 until numberOfVertices) { - // create the cladding faces of the frustum - // because the vertex vectors of the base and top polygon are in 2D they need to be extended to a 3D vector - val vertices = - basePolygon.vertex(i).to3D(0, 2) :: - basePolygon.vertex(i - 1).to3D(0, 2) :: - topPolygon.vertex(i - 1).to3D(height, 2) :: - topPolygon.vertex(i).to3D(height, 2) :: - Nil - polygons ::= new Polygon(vertices) - } - // return the list of polygons, but first prepend the base polygon - new Polygon(basePolygon.to3D(0, 2)) :: polygons - } - - override def toString = polygons.mkString("[", ",", "]") -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/OrientedPosition.scala b/util/src/main/scala/com/scalableminds/util/geometry/OrientedPosition.scala deleted file mode 100644 index 9a793c4976f..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/OrientedPosition.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.scalableminds.util.geometry - -case class OrientedPosition(translation: Vector3D, direction: Vector3D) - -object OrientedPosition { - - def default = OrientedPosition(Vector3D(0, 0, 0), Vector3D(0, 0, 0)) -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Point3D.scala b/util/src/main/scala/com/scalableminds/util/geometry/Point3D.scala deleted file mode 100644 index d4c789cf07e..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/Point3D.scala +++ /dev/null @@ -1,113 +0,0 @@ -package com.scalableminds.util.geometry - -import play.api.libs.json.Json._ -import play.api.libs.json._ - -trait GenericPosition { - def x: Int - def y: Int - def z: Int -} - -case class Point3D(x: Int, y: Int, z: Int) { - def scale(f: (Int, Int) => Int) = - Point3D(f(x, 0), f(y, 1), f(z, 2)) - - def scale(s: Int): Point3D = - Point3D(x * s, y * s, z * s) - - def scale(s: Float): Point3D = - Point3D((x * s).toInt, (y * s).toInt, (z * s).toInt) - - def <=(o: Point3D): Boolean = - x <= o.x && y <= o.y && z <= o.z - - def hasGreaterCoordinateAs(other: Point3D) = - x > other.x || y > other.y || z > other.z - - def isIsotropic: Boolean = - x == y && y == z - - override def toString = "(%d, %d, %d)".format(x, y, z) - - def toList = List(x, y, z) - - def move(dx: Int, dy: Int, dz: Int) = - Point3D(x + dx, y + dy, z + dz) - - def dx(d: Int) = - Point3D(x + d, y, z) - - def dy(d: Int) = - Point3D(x, y + d, z) - - def dz(d: Int) = - Point3D(x, y, z + d) - - def move(o: Point3D): Point3D = - move(o.x, o.y, o.z) - - def negate = Point3D(-x, -y, -z) - - def to(bottomRight: Point3D) = - range(bottomRight, _ to _) - - def until(bottomRight: Point3D) = - range(bottomRight, _ until _) - - def maxDim = Math.max(Math.max(x, y), z) - - private def range(other: Point3D, func: (Int, Int) => Range) = - for { - x <- func(x, other.x) - y <- func(y, other.y) - z <- func(z, other.z) - } yield { - Point3D(x, y, z) - } -} - -object Point3D { - val formRx = "\\s*([0-9]+),\\s*([0-9]+),\\s*([0-9]+)\\s*".r - def toForm(p: Point3D) = Some("%d, %d, %d".format(p.x, p.y, p.z)) - - def apply(t: (Int, Int, Int)): Point3D = - Point3D(t._1, t._2, t._3) - - def fromForm(s: String) = - s match { - case formRx(x, y, z) => - Point3D(Integer.parseInt(x), Integer.parseInt(y), Integer.parseInt(z)) - case _ => - null - } - - def fromArray[T <% Int](array: Array[T]) = - if (array.size >= 3) - Some(Point3D(array(0), array(1), array(2))) - else - None - - def fromList(l: List[Int]) = - fromArray(l.toArray) - - implicit object Point3DReads extends Reads[Point3D] { - def reads(json: JsValue) = json match { - case JsArray(ts) if ts.size == 3 => - val c = ts.map(fromJson[Int](_)).flatMap(_.asOpt) - if (c.size != 3) - JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.array.invalidContent")))) - else - JsSuccess(Point3D(c(0), c(1), c(2))) - case _ => - JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.expected.point3DArray")))) - } - } - - implicit object Point3DWrites extends Writes[Point3D] { - def writes(v: Point3D) = { - val l = List(v.x, v.y, v.z) - JsArray(l.map(toJson(_))) - } - } -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Polygon.scala b/util/src/main/scala/com/scalableminds/util/geometry/Polygon.scala deleted file mode 100644 index 9e4454f24bf..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/Polygon.scala +++ /dev/null @@ -1,70 +0,0 @@ -package com.scalableminds.util.geometry - -import play.api.libs.json.Json._ -import play.api.libs.json._ - -import scala.math._ - -/** - * Representation of a regular polygon in a 2 dimensional space - * n - is the number of vertices and a is the apothem - * http://en.wikipedia.org/wiki/Regular_polygon - */ -class RegularPolygon(numberOfVertices: Int, apothem: Int) { - val vertices = { - // angle between two diagonals - val alpha = 2 * Pi / numberOfVertices - // vector to the first vertex - var pointVector = new Vector2D(-tan(alpha / 2) * apothem, apothem) - var vertices = pointVector :: Nil - // length of the polygons sides - val s = 2 * tan(alpha / 2) * apothem - // vector which is used to generate the following vertices - // through iteration, rotating and vector addition - var v = new Vector2D(-s, 0) - for (i <- 1 until numberOfVertices) { - v = v.rotate(alpha) - pointVector = pointVector + v - vertices = vertices ::: pointVector :: Nil - } - vertices - } - - /** - * Returns the vertex at the given index. Negative indices are used - * to navigate clockwise - */ - def vertex(index: Int) = { - val i = (index % vertices.size) - if (i < 0) vertices(vertices.size + i) else vertices(i) - } - // convert polygon to a valid json representation - def to3D(value: Int, position: Int) = - vertices.map(v => v.to3D(value, position)) -} - -/** - * Simple polygon implementation - */ -class Polygon(val vertices: List[Vector3D]) { - val normalVector = { - if (vertices.size < 3) - throw new IllegalStateException("Not a valid Polygon: " + vertices) - else - (vertices(0) - vertices(1)) x (vertices(2) - vertices(1)) - } - - def transformAffine(matrix: Array[Float]): Polygon = - new Polygon(vertices.map(_.transformAffine(matrix))) - - val d = normalVector ° vertices(0) - - override def toString = - vertices.toString -} -object Polygon { - // json converter - implicit object PolygonWrites extends Writes[Polygon] { - def writes(p: Polygon) = JsArray(p.vertices.map(v => toJson(v.toVector3I))) - } -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Scale.scala b/util/src/main/scala/com/scalableminds/util/geometry/Scale.scala deleted file mode 100644 index 726b8c2b2c8..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/Scale.scala +++ /dev/null @@ -1,53 +0,0 @@ -package com.scalableminds.util.geometry - -import play.api.libs.json._ - -case class Scale(x: Float, y: Float, z: Float) { - - def isValid: Boolean = - x > 0 && y > 0 && z > 0 - - override def toString() = - s"($x, $y, $z)" - - def toVector: Vector3D = Vector3D(x, y, z) -} - -object Scale { - val comp = "\\s*([0-9]+(?:\\.[0-9]+)?)" - val formRx = s"$comp,$comp,$comp\\s*".r - - val scaleReads = - (__.read[List[Float]]).filter(_.size >= 3).map { l => - Scale(l(0), l(1), l(2)) - } - - val scaleWrites: Writes[Scale] = - Writes { s: Scale => - JsArray(List(JsNumber(s.x), JsNumber(s.y), JsNumber(s.z))) - } - - implicit val scaleFormat = Format(scaleReads, scaleWrites) - - def default = Scale(0, 0, 0) - - def toForm(s: Scale) = Some(s"${s.x}, ${s.y}, ${s.z}") - - def fromForm(s: String) = - s match { - case formRx(x, y, z) => - Scale(java.lang.Float.parseFloat(x), java.lang.Float.parseFloat(y), java.lang.Float.parseFloat(z)) - case _ => - null - } - - def fromArray[T <% Float](array: Array[T]) = - if (array.size >= 3) - Some(Scale(array(0), array(1), array(2))) - else - None - - def fromList(l: List[Float]) = - fromArray(l.toArray) - -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/TransformationMatrix.scala b/util/src/main/scala/com/scalableminds/util/geometry/TransformationMatrix.scala deleted file mode 100644 index 063a7956b44..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/TransformationMatrix.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.scalableminds.util.geometry - -case class MatrixBase3D(x: Vector3D, y: Vector3D, z: Vector3D) - -case class TransformationMatrix(value: Array[Float]) { - val width = math.sqrt(TransformationMatrix.defaultSize).toInt - val height = width -} - -object TransformationMatrix { - val defaultSize = 16 - - def identity = - TransformationMatrix(Array[Float]( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1)) - - def apply(pos: Vector3D, rotationBase: MatrixBase3D): TransformationMatrix = { - - val MatrixBase3D(ny, nx, nz) = rotationBase - - TransformationMatrix(Array( - nx.x, ny.x, nz.x , pos.x, - nx.y, ny.y, nz.y , pos.y, - nx.z * 11.24 / 28, ny.z * 11.24 / 28, nz.z * 11.24 / 28, pos.z, - 0, 0, 0, 1 - ).map(_.toFloat)) - } -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Double.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Double.scala new file mode 100644 index 00000000000..a49e3d439fc --- /dev/null +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Double.scala @@ -0,0 +1,107 @@ +package com.scalableminds.util.geometry + +import com.scalableminds.util.tools.Math._ +import play.api.libs.json.Json._ +import play.api.libs.json._ + +import scala.math._ + +case class Vec3Double(x: Double, y: Double, z: Double) { + + def normalize: Vec3Double = { + val length = sqrt(square(x) + square(y) + square(z)) + if (length != 0) + Vec3Double(x / length, y / length, z / length) + else + this + } + + def -(o: Vec3Double): Vec3Double = + new Vec3Double(x - o.x, y - o.y, z - o.z) + + def +(o: Vec3Double): Vec3Double = + new Vec3Double(x + o.x, y + o.y, z + o.z) + + def x(o: Vec3Double): Vec3Double = + new Vec3Double(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x) + + def *(o: Double): Vec3Double = Vec3Double(x * o, y * o, z * o) + + def *:(o: Double): Vec3Double = this.*(o) + + // Element-wise multiplication + def *(o: Vec3Double): Vec3Double = Vec3Double(x * o.x, y * o.y, z * o.z) + + // Element-wise division + def /(o: Vec3Double): Vec3Double = Vec3Double(x / o.x, y / o.y, z / o.z) + + /** + * Transforms this vector using a transformation matrix + */ + def transformAffine(matrix: Array[Float]): Vec3Double = { + // see rotation matrix and helmert-transformation for more details + val nx = matrix(0) * x + matrix(4) * y + matrix(8) * z + matrix(12) + val ny = matrix(1) * x + matrix(5) * y + matrix(9) * z + matrix(13) + val nz = matrix(2) * x + matrix(6) * y + matrix(10) * z + matrix(14) + Vec3Double(nx, ny, nz) + } + + def rotate(matrix: List[Float]): Vec3Double = { + // see rotation matrix and helmert-transformation for more details + val nx = matrix(0) * x + matrix(4) * y + matrix(8) * z + val ny = matrix(1) * x + matrix(5) * y + matrix(9) * z + val nz = matrix(2) * x + matrix(6) * y + matrix(10) * z + Vec3Double(nx, ny, nz) + } + + def toVec3Int: Vec3Int = Vec3Int(x.toInt, y.toInt, z.toInt) + + def °(o: Vec3Double): Double = x * o.x + y * o.y + z * o.z + + def °(o: Tuple3[Double, Double, Double]): Double = x * o._1 + y * o._2 + z * o._3 + + def toTuple: (Double, Double, Double) = (x, y, z) + + def toList = List(x, y, z) + + def isStrictlyPositive: Boolean = x > 0 && y > 0 && z > 0 + + override def toString = s"($x, $y, $z)" +} + +object Vec3Double { + def apply(p: Vec3Int): Vec3Double = + Vec3Double(p.x, p.y, p.z) + + def apply(p: (Double, Double, Double)): Vec3Double = + Vec3Double(p._1, p._2, p._3) + + def apply(from: Vec3Int, to: Vec3Int): Vec3Double = + Vec3Double(to) - Vec3Double(from) + + def fromArray[T <% Double](array: Array[T]): Option[Vec3Double] = + if (array.length >= 3) + Some(Vec3Double(array(0), array(1), array(2))) + else + None + + def fromList(l: List[Double]): Option[Vec3Double] = + fromArray(l.toArray) + + implicit object Vector3DReads extends Format[Vec3Double] { + def reads(json: JsValue): JsResult[Vec3Double] = json match { + case JsArray(ts) if ts.size == 3 => + ts.toList.map(fromJson[Double](_)) match { + case JsSuccess(a, _) :: JsSuccess(b, _) :: JsSuccess(c, _) :: _ => + JsSuccess(Vec3Double(a, b, c)) + case x => + JsError("Invalid array content: " + x) + } + case _ => + JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.listExpected")))) + } + + def writes(v: Vec3Double): JsValue = + Json.toJson(List(v.x, v.y, v.z)) + } +} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala new file mode 100644 index 00000000000..2a40f56e1cf --- /dev/null +++ b/util/src/main/scala/com/scalableminds/util/geometry/Vec3Int.scala @@ -0,0 +1,90 @@ +package com.scalableminds.util.geometry + +import play.api.libs.json.Json._ +import play.api.libs.json._ + +case class Vec3Int(x: Int, y: Int, z: Int) { + def scale(s: Int): Vec3Int = + Vec3Int(x * s, y * s, z * s) + + def scale(s: Float): Vec3Int = + Vec3Int((x * s).toInt, (y * s).toInt, (z * s).toInt) + + def <=(other: Vec3Int): Boolean = + x <= other.x && y <= other.y && z <= other.z + + def isIsotropic: Boolean = + x == y && y == z + + override def toString: String = "(%d, %d, %d)".format(x, y, z) + + def toList = List(x, y, z) + + def move(dx: Int, dy: Int, dz: Int) = + Vec3Int(x + dx, y + dy, z + dz) + + def move(other: Vec3Int): Vec3Int = + move(other.x, other.y, other.z) + + def negate = Vec3Int(-x, -y, -z) + + def to(bottomRight: Vec3Int) = + range(bottomRight, _ to _) + + def until(bottomRight: Vec3Int) = + range(bottomRight, _ until _) + + def maxDim: Int = Math.max(Math.max(x, y), z) + + private def range(other: Vec3Int, func: (Int, Int) => Range) = + for { + x <- func(x, other.x) + y <- func(y, other.y) + z <- func(z, other.z) + } yield Vec3Int(x, y, z) +} + +object Vec3Int { + val formRx = "\\s*([0-9]+),\\s*([0-9]+),\\s*([0-9]+)\\s*".r + def toForm(p: Vec3Int) = Some("%d, %d, %d".format(p.x, p.y, p.z)) + + def apply(t: (Int, Int, Int)): Vec3Int = + Vec3Int(t._1, t._2, t._3) + + def fromForm(s: String) = + s match { + case formRx(x, y, z) => + Vec3Int(Integer.parseInt(x), Integer.parseInt(y), Integer.parseInt(z)) + case _ => + null + } + + def fromArray[T <% Int](array: Array[T]) = + if (array.size >= 3) + Some(Vec3Int(array(0), array(1), array(2))) + else + None + + def fromList(l: List[Int]) = + fromArray(l.toArray) + + implicit object Vec3IntReads extends Reads[Vec3Int] { + def reads(json: JsValue) = json match { + case JsArray(ts) if ts.size == 3 => + val c = ts.map(fromJson[Int](_)).flatMap(_.asOpt) + if (c.size != 3) + JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.array.invalidContent")))) + else + JsSuccess(Vec3Int(c(0), c(1), c(2))) + case _ => + JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.expected.vec3IntArray")))) + } + } + + implicit object Vec3IntWrites extends Writes[Vec3Int] { + def writes(v: Vec3Int) = { + val l = List(v.x, v.y, v.z) + JsArray(l.map(toJson(_))) + } + } +} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vector2D.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vector2D.scala deleted file mode 100644 index 0d5a0efbd60..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vector2D.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.scalableminds.util.geometry - -import scala.math._ - -/** - * Vector in 2D space, which is able to handle vector addition and rotation - */ -class Vector2D(val x: Double, val y: Double) { - - def +(b: Vector2D) = new Vector2D(x + b.x, y + b.y) - - def rotate(a: Double) = new Vector2D(x * cos(a) - y * sin(a), x * sin(a) + y * cos(a)) - - /** - * Add another dimension to the vector. value specifies the index - * where the dimension gets added and position should be one of 0,1 or 2 - * and indicates whether to place the new value on x,y or z coordinate. - */ - def to3D(value: Double, position: Int) = position match { - case 0 => new Vector3D(value, x, y) - case 1 => new Vector3D(x, value, y) - case 2 => new Vector3D(x, y, value) - } - - override def equals(v: Any): Boolean = v match { - case v: Vector2D => x == x && v.y == y - case _ => false - } - - override def toString = "[%d,%d]".format(x.round, y.round) -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vector3D.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vector3D.scala deleted file mode 100644 index 2ab8fd8e2b5..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vector3D.scala +++ /dev/null @@ -1,112 +0,0 @@ -package com.scalableminds.util.geometry - -import com.scalableminds.util.tools.Math._ -import play.api.libs.json.Json._ -import play.api.libs.json._ - -import scala.math._ - -/** - * Vector in 3D space - */ -case class Vector3D(x: Double = 0, y: Double = 0, z: Double = 0) { - - def normalize = { - val length = sqrt(square(x) + square(y) + square(z)) - if (length != 0) - Vector3D(x / length, y / length, z / length) - else - this - } - - def neg = Vector3D(-x, -y, -z) - - def -(o: Vector3D): Vector3D = - new Vector3D(x - o.x, y - o.y, z - o.z) - - def +(o: Vector3D): Vector3D = - new Vector3D(x + o.x, y + o.y, z + o.z) - - def x(o: Vector3D): Vector3D = - new Vector3D(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x) - - def *(o: Double) = Vector3D(x * o, y * o, z * o) - - def *:(o: Double) = this.*(o) - - // Element-wise multiplication - def *(o: Vector3D) = Vector3D(x * o.x, y * o.y, z * o.z) - - // Element-wise division - def /(o: Vector3D) = Vector3D(x / o.x, y / o.y, z / o.z) - - /** - * Transforms this vector using a transformation matrix - */ - def transformAffine(matrix: Array[Float]): Vector3D = { - // see rotation matrix and helmert-transformation for more details - val nx = matrix(0) * x + matrix(4) * y + matrix(8) * z + matrix(12) - val ny = matrix(1) * x + matrix(5) * y + matrix(9) * z + matrix(13) - val nz = matrix(2) * x + matrix(6) * y + matrix(10) * z + matrix(14) - Vector3D(nx, ny, nz) - } - - def rotate(matrix: List[Float]): Vector3D = { - // see rotation matrix and helmert-transformation for more details - val nx = matrix(0) * x + matrix(4) * y + matrix(8) * z - val ny = matrix(1) * x + matrix(5) * y + matrix(9) * z - val nz = matrix(2) * x + matrix(6) * y + matrix(10) * z - Vector3D(nx, ny, nz) - } - - def toVector3I = Vector3I(x.round.toInt, y.round.toInt, z.round.toInt) - - def toPoint3D = Point3D(x.toInt, y.toInt, z.toInt) - - def °(o: Vector3D) = x * o.x + y * o.y + z * o.z - - def °(o: Tuple3[Double, Double, Double]) = x * o._1 + y * o._2 + z * o._3 - - def toTuple = (x, y, z) - - def toList = List(x, y, z) - - override def toString = s"($x, $y, $z)" -} - -object Vector3D { - def apply(p: Point3D): Vector3D = - Vector3D(p.x, p.y, p.z) - - def apply(p: (Double, Double, Double)): Vector3D = - Vector3D(p._1, p._2, p._3) - - def apply(from: Point3D, to: Point3D): Vector3D = - Vector3D(to) - Vector3D(from) - - def fromArray[T <% Double](array: Array[T]) = - if (array.size >= 3) - Some(Vector3D(array(0), array(1), array(2))) - else - None - - def fromList(l: List[Double]) = - fromArray(l.toArray) - - implicit object Vector3DReads extends Format[Vector3D] { - def reads(json: JsValue) = json match { - case JsArray(ts) if ts.size == 3 => - ts.toList.map(fromJson[Double](_)) match { - case JsSuccess(a, _) :: JsSuccess(b, _) :: JsSuccess(c, _) :: _ => - JsSuccess(Vector3D(a, b, c)) - case x => - JsError("Invalid array content: " + x) - } - case _ => - JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.listExpected")))) - } - - def writes(v: Vector3D) = - Json.toJson(List(v.x, v.y, v.z)) - } -} diff --git a/util/src/main/scala/com/scalableminds/util/geometry/Vector3I.scala b/util/src/main/scala/com/scalableminds/util/geometry/Vector3I.scala deleted file mode 100644 index cf368c40b15..00000000000 --- a/util/src/main/scala/com/scalableminds/util/geometry/Vector3I.scala +++ /dev/null @@ -1,57 +0,0 @@ -package com.scalableminds.util.geometry - -import play.api.libs.json.Json._ -import play.api.libs.json._ - -import scala.math._ - -case class Vector3I(val x: Int, val y: Int, val z: Int) { - - def -(o: Vector3I) = Vector3I(x - o.x, y - o.y, z - o.z) - def +(o: Vector3I) = Vector3I(x + o.x, y + o.y, z + o.z) - - def fillGapTill(dest: Vector3I): List[Vector3I] = { - val dx = x - dest.x - val dy = y - dest.y - val dz = z - dest.z - - val maxSize = max(dx.abs, max(dy.abs, dz.abs)) - - val xList = List.fill(dx.abs)(dx.signum) ::: List.fill(maxSize - dx.abs)(0) - val yList = List.fill(dy.abs)(dy.signum) ::: List.fill(maxSize - dy.abs)(0) - val zList = List.fill(dz.abs)(dz.signum) ::: List.fill(maxSize - dz.abs)(0) - - List(xList, yList, zList).transpose.map(Vector3I.IntListToVector3I) - } -} - -object Vector3I { - - val defaultSize = 3 - - implicit def Vector3IToIntTuple(v: Vector3I) = (v.x, v.y, v.z) - implicit def Vector3IToIntList(v: Vector3I) = List(v.x, v.y, v.z) - implicit def Vector3IToIntArray(v: Vector3I) = Array(v.x, v.y, v.z) - implicit def IntListToVector3I(l: List[Int]) = Vector3I(l(0), l(1), l(2)) - implicit def IntListToVector3I(l: Array[Int]) = Vector3I(l(0), l(1), l(2)) - - // json converter - implicit object Vector3IWrites extends Writes[Vector3I] { - def writes(v: Vector3I) = { - val l = List(v.x, v.y, v.z) - JsArray(l.map(toJson(_))) - } - } - implicit object Vector3IReads extends Reads[Vector3I] { - def reads(json: JsValue) = json match { - case JsArray(ts) if ts.size == 3 => - val c = ts.map(fromJson[Int](_)).flatMap(_.asOpt) - if (c.size != 3) - JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.array.invalidContent")))) - else - JsSuccess(Vector3I(c(0), c(1), c(2))) - case _ => - JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.expected.point3DArray")))) - } - } -} diff --git a/util/src/main/scala/com/scalableminds/util/image/Color.scala b/util/src/main/scala/com/scalableminds/util/image/Color.scala index 0fce7d05715..d399c4f2f5f 100644 --- a/util/src/main/scala/com/scalableminds/util/image/Color.scala +++ b/util/src/main/scala/com/scalableminds/util/image/Color.scala @@ -37,7 +37,7 @@ object Color { case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.array.invalidContent")))) } case _ => - JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.expected.point3DArray")))) + JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.expected.vec3IntArray")))) } } diff --git a/util/src/main/scala/com/scalableminds/util/io/ZipIO.scala b/util/src/main/scala/com/scalableminds/util/io/ZipIO.scala index 0576bce55a9..56dc16b73a8 100644 --- a/util/src/main/scala/com/scalableminds/util/io/ZipIO.scala +++ b/util/src/main/scala/com/scalableminds/util/io/ZipIO.scala @@ -93,8 +93,12 @@ object ZipIO extends LazyLogging { def zip(sources: List[NamedStream], out: OutputStream)(implicit ec: ExecutionContext): Future[Unit] = zip(sources.toIterator, out) - def zip(sources: Iterator[NamedStream], out: OutputStream)(implicit ec: ExecutionContext): Future[Unit] = { + def zip(sources: Iterator[NamedStream], out: OutputStream, level: Int = -1)( + implicit ec: ExecutionContext): Future[Unit] = { val zip = startZip(out) + if (level != -1) { + zip.stream.setLevel(level) + } if (sources.nonEmpty) { for { _ <- zipIterator(sources, zip) diff --git a/util/src/main/scala/com/scalableminds/util/mvc/ExtendedController.scala b/util/src/main/scala/com/scalableminds/util/mvc/ExtendedController.scala index 3f92be897b9..92e2ec8425c 100644 --- a/util/src/main/scala/com/scalableminds/util/mvc/ExtendedController.scala +++ b/util/src/main/scala/com/scalableminds/util/mvc/ExtendedController.scala @@ -35,10 +35,16 @@ trait ResultBox extends I18nSupport with Formatter { implicit messages: MessagesProvider): String = chain match { case Full(failure) => val serverTimeMsg = if (includeTime) "[Server Time " + formatDate(System.currentTimeMillis()) + "] " else "" - serverTimeMsg + " <~ " + Messages(failure.msg) + formatChain(failure.chain, includeTime = false) + serverTimeMsg + " <~ " + formatFailure(failure) + formatChain(failure.chain, includeTime = false) case _ => "" } + private def formatFailure(failure: Failure)(implicit messages: MessagesProvider): String = + failure match { + case ParamFailure(msg, _, _, param) => Messages(msg) + " " + param.toString + case Failure(msg, _, _) => Messages(msg) + } + def jsonMessages(msgs: JsArray): JsObject = Json.obj("messages" -> msgs) } @@ -153,6 +159,5 @@ trait ExtendedController with FoxImplicits with ResultImplicits with Status - with WithHighlightableResult with WithFilters with I18nSupport diff --git a/util/src/main/scala/com/scalableminds/util/mvc/WithHighlightableResult.scala b/util/src/main/scala/com/scalableminds/util/mvc/WithHighlightableResult.scala deleted file mode 100644 index 9961cfb6306..00000000000 --- a/util/src/main/scala/com/scalableminds/util/mvc/WithHighlightableResult.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.scalableminds.util.mvc - -import play.api.http.HeaderNames._ -import play.api.mvc.Result - -trait WithHighlightableResult { - - implicit class HighlightableResult(r: Result) { - def highlighting(elementId: String) = { - val location = r.header.headers.get(LOCATION) getOrElse "" - r.withHeaders(LOCATION -> s"$location#$elementId") - } - } - -} diff --git a/util/src/main/scala/com/scalableminds/util/security/InsecureSSLSocketFactory.scala b/util/src/main/scala/com/scalableminds/util/security/InsecureSSLSocketFactory.scala deleted file mode 100644 index aec14270e46..00000000000 --- a/util/src/main/scala/com/scalableminds/util/security/InsecureSSLSocketFactory.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.scalableminds.util.security - -import java.security.cert.X509Certificate -import javax.net.ssl.{HttpsURLConnection, SSLContext, TrustManager, X509TrustManager} - -object InsecureSSLSocketFactory { - lazy val default = HttpsURLConnection.getDefaultSSLSocketFactory() - lazy val socketFactory = { - val trustAllCerts = Array[TrustManager](new X509TrustManager() { - override def checkClientTrusted(chain: Array[X509Certificate], authType: String) {} - - override def checkServerTrusted(chain: Array[X509Certificate], authType: String) {} - - override def getAcceptedIssuers(): Array[X509Certificate] = - return null; - }) - - // Install the all-trusting trust manager - val sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - // Create an ssl socket factory with our all-trusting manager - sslContext.getSocketFactory(); - } - - def load() { - HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory) - } - - def unload() { - HttpsURLConnection.setDefaultSSLSocketFactory(default) - } - - def usingSelfSignedCert(block: => Unit) { - load() - block - unload() - } -} diff --git a/util/src/main/scala/com/scalableminds/util/tools/FileExtensionFilter.scala b/util/src/main/scala/com/scalableminds/util/tools/FileExtensionFilter.scala deleted file mode 100644 index b3acdc9ffec..00000000000 --- a/util/src/main/scala/com/scalableminds/util/tools/FileExtensionFilter.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.scalableminds.util.tools - -import java.io.{File, FilenameFilter} - -import scala.util.matching.Regex - -class FileExtensionFilter(fileExtension: String) extends FilenameFilter { - override def accept(dir: File, name: String) = name.endsWith(fileExtension) -} - -class FileRegExFilter(regEx: Regex) extends FilenameFilter { - override def accept(dir: File, name: String) = (regEx findFirstIn name).nonEmpty -} diff --git a/util/src/main/scala/com/scalableminds/util/tools/Interpolator.scala b/util/src/main/scala/com/scalableminds/util/tools/Interpolator.scala deleted file mode 100644 index 27b1bc8bfc6..00000000000 --- a/util/src/main/scala/com/scalableminds/util/tools/Interpolator.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.scalableminds.util.tools - -import com.scalableminds.util.geometry.Vector3D - -object Interpolator { - def triLerp(d: Vector3D, p: Array[Array[Double]], elements: Int): Array[Double] = { - var e = 0 - val r = new Array[Double](elements) - while (e < elements) { - r(e) = - p(0)(e) * (1 - d.x) * (1 - d.y) * (1 - d.z) + - p(4)(e) * d.x * (1 - d.y) * (1 - d.z) + - p(2)(e) * (1 - d.x) * d.y * (1 - d.z) + - p(6)(e) * d.x * d.y * (1 - d.z) + - p(1)(e) * (1 - d.x) * (1 - d.y) * d.z + - p(5)(e) * d.x * (1 - d.y) * d.z + - p(3)(e) * (1 - d.x) * d.y * d.z + - p(7)(e) * d.x * d.y * d.z - e += 1 - } - r - } -} diff --git a/util/src/main/scala/com/scalableminds/util/tools/Reflect.scala b/util/src/main/scala/com/scalableminds/util/tools/Reflect.scala deleted file mode 100644 index 544b3c19ea8..00000000000 --- a/util/src/main/scala/com/scalableminds/util/tools/Reflect.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.scalableminds.util.tools - -import scala.util.control.NonFatal - -/** - * Collection of internal reflection utilities which may or may not be - * available (most services specific to HotSpot, but fails gracefully). - */ -object Reflect { - - /** - * This optionally holds a function which looks N levels above itself - * on the call stack and returns the `Class[_]` object for the code - * executing in that stack frame. Implemented using - * `sun.reflect.Reflection.getCallerClass` if available, None otherwise. - * - * Hint: when comparing to Thread.currentThread.getStackTrace, add two levels. - */ - val getCallerClass: Option[Int ⇒ Class[_]] = { - try { - val c = Class.forName("sun.reflect.Reflection"); - val m = c.getMethod("getCallerClass", Array(classOf[Int]): _*) - Some((i: Int) ⇒ m.invoke(null, Array[AnyRef](i.asInstanceOf[java.lang.Integer]): _*).asInstanceOf[Class[_]]) - } catch { - case NonFatal(e) ⇒ None - } - } -} diff --git a/util/src/main/scala/com/scalableminds/util/tools/StartableActor.scala b/util/src/main/scala/com/scalableminds/util/tools/StartableActor.scala deleted file mode 100644 index 9d824a3aaac..00000000000 --- a/util/src/main/scala/com/scalableminds/util/tools/StartableActor.scala +++ /dev/null @@ -1,11 +0,0 @@ -package com.scalableminds.util.tools - -import akka.actor.{Actor, Props} - -import scala.reflect.ClassTag - -trait StartableActor[T <: Actor] { - def name: String - def start(implicit sys: akka.actor.ActorSystem, tag: ClassTag[T]) = - sys.actorOf(Props[T], name) -} diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala index e1cdeb5072b..6b844fe27b5 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala @@ -6,7 +6,7 @@ import java.util.Base64 import akka.stream.scaladsl.StreamConverters import com.google.inject.Inject -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.image.{ImageCreator, ImageCreatorParameters, JPEGWriter} import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.DataStoreConfig @@ -106,12 +106,12 @@ class BinaryDataController @Inject()( @ApiParam(value = "Name of the dataset’s organization", required = true) organizationName: String, @ApiParam(value = "Dataset name", required = true) dataSetName: String, @ApiParam(value = "Layer name of the dataset", required = true) dataLayerName: String, - @ApiParam(value = "x coordinate of the top-left corner of the bounding box", required = true) x: Int, - @ApiParam(value = "y coordinate of the top-left corner of the bounding box", required = true) y: Int, - @ApiParam(value = "z coordinate of the top-left corner of the bounding box", required = true) z: Int, - @ApiParam(value = "width of the bounding box", required = true) width: Int, - @ApiParam(value = "height of the bounding box", required = true) height: Int, - @ApiParam(value = "depth of the bounding box", required = true) depth: Int, + @ApiParam(value = "Mag1 x coordinate of the top-left corner of the bounding box", required = true) x: Int, + @ApiParam(value = "Mag1 y coordinate of the top-left corner of the bounding box", required = true) y: Int, + @ApiParam(value = "Mag1 z coordinate of the top-left corner of the bounding box", required = true) z: Int, + @ApiParam(value = "Target-mag width of the bounding box", required = true) width: Int, + @ApiParam(value = "Target-mag height of the bounding box", required = true) height: Int, + @ApiParam(value = "Target-mag depth of the bounding box", required = true) depth: Int, @ApiParam(value = "Exponent of the dataset mag (e.g. 4 for mag 16-16-8)", required = true) resolution: Int, @ApiParam(value = "If true, use lossy compression by sending only half-bytes of the data") halfByte: Boolean ): Action[AnyContent] = Action.async { implicit request => @@ -184,7 +184,7 @@ class BinaryDataController @Inject()( new VoxelPosition(x * cubeSize * resolution, y * cubeSize * resolution, z * cubeSize * resolution, - Point3D(resolution, resolution, resolution)), + Vec3Int(resolution, resolution, resolution)), cubeSize, cubeSize, cubeSize @@ -399,7 +399,7 @@ class BinaryDataController @Inject()( segmentationLayer, request.body.cuboid(dataLayer), request.body.segmentId, - request.body.voxelDimensions, + request.body.subsamplingStrides, request.body.scale, request.body.mapping, request.body.mappingType diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWBucketStreamSink.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWBucketStreamSink.scala index 8105acc375c..300b51a4e52 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWBucketStreamSink.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWBucketStreamSink.scala @@ -2,7 +2,7 @@ package com.scalableminds.webknossos.datastore.dataformats.wkw import java.io.DataOutputStream -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.models.BucketPosition import com.scalableminds.webknossos.datastore.models.datasource.DataLayer import com.scalableminds.util.io.{NamedFunctionStream, NamedStream} @@ -16,7 +16,7 @@ class WKWBucketStreamSink(val layer: DataLayer) extends WKWDataFormatHelper { def apply(bucketStream: Iterator[(BucketPosition, Array[Byte])]): Iterator[NamedStream] = { val (voxelType, numChannels) = WKWDataFormat.elementClassToVoxelType(layer.elementClass) val header = WKWHeader(1, DataLayer.bucketLength, BlockType.LZ4, voxelType, numChannels) - val resolutions = new mutable.HashSet[Point3D]() + val resolutions = new mutable.HashSet[Vec3Int]() bucketStream.map { case (bucket, data) => val filePath = wkwFilePath(bucket.toCube(bucket.bucketLength)).toString diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala index 9c83b8b7323..f98bf268e19 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.datastore.dataformats.wkw import java.nio.file.Path import com.scalableminds.webknossos.datastore.models.datasource.{Category, DataLayer, SegmentationLayer} -import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.ExtendedTypes._ import com.scalableminds.webknossos.datastore.services.{DataSourceImportReport, DataSourceImporter} @@ -56,7 +56,7 @@ object WKWDataFormat extends DataSourceImporter with WKWDataFormatHelper { } private def exploreResolutions(baseDir: Path)( - implicit report: DataSourceImportReport[Path]): Box[List[(WKWHeader, Either[Int, Point3D])]] = + implicit report: DataSourceImportReport[Path]): Box[List[(WKWHeader, Either[Int, Vec3Int])]] = PathUtils.listDirectories(baseDir, resolutionDirFilter).flatMap { resolutionDirs => val resolutionHeaders = resolutionDirs.sortBy(resolutionDirSortingKey).map { resolutionDir => val resolutionIntOrPoint3 = parseResolutionName(resolutionDir).get @@ -75,7 +75,7 @@ object WKWDataFormat extends DataSourceImporter with WKWDataFormatHelper { } else Full(list)) } - private def extractHeaderParameters(resolutions: List[(WKWHeader, Either[Int, Point3D])])( + private def extractHeaderParameters(resolutions: List[(WKWHeader, Either[Int, Vec3Int])])( implicit report: DataSourceImportReport[Path]): Box[((VoxelType.Value, Int), List[WKWResolution])] = { val headers = resolutions.map(_._1) val voxelTypes = headers.map(_.voxelType).toSet @@ -125,7 +125,7 @@ object WKWDataFormat extends DataSourceImporter with WKWDataFormatHelper { (xMin, xMax) = xFiles.foldRight((getIntFromFilePath(xFile), 0))(minMaxValue) } yield { BoundingBox( - Point3D(xMin * multiplierX, yMin * multiplierY, zMin * multiplierZ), + Vec3Int(xMin * multiplierX, yMin * multiplierY, zMin * multiplierZ), xMax * multiplierX - xMin * multiplierX, yMax * multiplierY - yMin * multiplierY, zMax * multiplierZ - zMin * multiplierZ diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormatHelper.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormatHelper.scala index fbfc37ae0a0..0d5ba232217 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormatHelper.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormatHelper.scala @@ -2,7 +2,7 @@ package com.scalableminds.webknossos.datastore.dataformats.wkw import java.nio.file.{Path, Paths} -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.models.datasource.{DataLayer, DataSourceId, ElementClass} import com.scalableminds.webknossos.datastore.models.{BucketPosition, CubePosition} import com.scalableminds.webknossos.wrap.VoxelType @@ -29,7 +29,7 @@ trait WKWDataFormatHelper { .resolve(s"y${cube.y}") .resolve(s"x${cube.x}.$dataFileExtension") - private def formatResolution(resolution: Point3D, resolutionAsTripleOpt: Option[Boolean] = None): String = + private def formatResolution(resolution: Vec3Int, resolutionAsTripleOpt: Option[Boolean] = None): String = resolutionAsTripleOpt.map { resolutionAsTriple => if (resolutionAsTriple) s"${resolution.x}-${resolution.y}-${resolution.z}" else resolution.maxDim.toString @@ -38,7 +38,7 @@ trait WKWDataFormatHelper { } def wkwHeaderFilePath( - resolution: Point3D, + resolution: Vec3Int, dataSourceId: Option[DataSourceId] = None, dataLayerName: Option[String] = None, baseDir: Path = Paths.get("") @@ -69,13 +69,13 @@ trait WKWDataFormatHelper { } } - protected def parseResolution(resolutionStr: String): Option[Point3D] = + protected def parseResolution(resolutionStr: String): Option[Vec3Int] = resolutionStr.toIntOpt match { - case Some(resolutionInt) => Some(Point3D(resolutionInt, resolutionInt, resolutionInt)) + case Some(resolutionInt) => Some(Vec3Int(resolutionInt, resolutionInt, resolutionInt)) case None => val pattern = """(\d+)-(\d+)-(\d+)""".r resolutionStr match { - case pattern(x, y, z) => Some(Point3D(x.toInt, y.toInt, z.toInt)) + case pattern(x, y, z) => Some(Vec3Int(x.toInt, y.toInt, z.toInt)) case _ => None } } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataLayers.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataLayers.scala index bd34f73e43f..164ffd12b11 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataLayers.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataLayers.scala @@ -1,14 +1,14 @@ package com.scalableminds.webknossos.datastore.dataformats.wkw -import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration import com.scalableminds.webknossos.datastore.models.datasource.{DataFormat, _} import play.api.libs.json.{Json, OFormat} -case class WKWResolution(resolution: Either[Int, Point3D], cubeLength: Int) { - def resolutionAsPoint3D: Point3D = resolution match { +case class WKWResolution(resolution: Either[Int, Vec3Int], cubeLength: Int) { + def toVec3Int: Vec3Int = resolution match { case Left(r) => - Point3D(r, r, r) + Vec3Int(r, r, r) case Right(r) => r } @@ -26,10 +26,10 @@ trait WKWLayer extends DataLayer { def wkwResolutions: List[WKWResolution] - def resolutions: List[Point3D] = wkwResolutions.map(_.resolutionAsPoint3D) + def resolutions: List[Vec3Int] = wkwResolutions.map(_.toVec3Int) - def lengthOfUnderlyingCubes(resolution: Point3D): Int = - wkwResolutions.find(_.resolutionAsPoint3D == resolution).map(_.cubeLength).getOrElse(0) + def lengthOfUnderlyingCubes(resolution: Vec3Int): Int = + wkwResolutions.find(_.toVec3Int == resolution).map(_.cubeLength).getOrElse(0) } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/ProtoGeometryImplicits.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/ProtoGeometryImplicits.scala index 393505506f9..5b50843643b 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/ProtoGeometryImplicits.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/ProtoGeometryImplicits.scala @@ -1,40 +1,36 @@ package com.scalableminds.webknossos.datastore.helpers -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Vector3D} -import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing.{ElementClass => ProtoElementClass} -import com.scalableminds.webknossos.datastore.geometry.{ - BoundingBox => ProtoBoundingBox, - Point3D => ProtoPoint3D, - Vector3D => ProtoVector3D -} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int, Vec3Double} +import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing.{ElementClass => ElementClassProto} +import com.scalableminds.webknossos.datastore.geometry.{BoundingBoxProto, Vec3IntProto, Vec3DoubleProto} import com.scalableminds.webknossos.datastore.models.datasource.ElementClass trait ProtoGeometryImplicits { - implicit def point3DToProto(p: Point3D): ProtoPoint3D = ProtoPoint3D(p.x, p.y, p.z) + implicit def vec3IntToProto(p: Vec3Int): Vec3IntProto = Vec3IntProto(p.x, p.y, p.z) - implicit def point3DFromProto(p: ProtoPoint3D): Point3D = Point3D(p.x, p.y, p.z) + implicit def vec3IntFromProto(p: Vec3IntProto): Vec3Int = Vec3Int(p.x, p.y, p.z) - implicit def vector3DToProto(v: Vector3D): ProtoVector3D = ProtoVector3D(v.x, v.y, v.z) + implicit def vec3DoubleToProto(v: Vec3Double): Vec3DoubleProto = Vec3DoubleProto(v.x, v.y, v.z) - implicit def vector3DFromProto(v: ProtoVector3D): Vector3D = Vector3D(v.x, v.y, v.z) + implicit def vec3DoubleFromProto(v: Vec3DoubleProto): Vec3Double = Vec3Double(v.x, v.y, v.z) - implicit def boundingBoxToProto(bb: BoundingBox): ProtoBoundingBox = - ProtoBoundingBox(bb.topLeft, bb.width, bb.height, bb.depth) + implicit def boundingBoxToProto(bb: BoundingBox): BoundingBoxProto = + BoundingBoxProto(bb.topLeft, bb.width, bb.height, bb.depth) - implicit def boundingBoxFromProto(bb: ProtoBoundingBox): BoundingBox = + implicit def boundingBoxFromProto(bb: BoundingBoxProto): BoundingBox = BoundingBox(bb.topLeft, bb.width, bb.height, bb.depth) - implicit def boundingBoxOptToProto(bbOpt: Option[BoundingBox]): Option[ProtoBoundingBox] = + implicit def boundingBoxOptToProto(bbOpt: Option[BoundingBox]): Option[BoundingBoxProto] = bbOpt.map(boundingBoxToProto) - implicit def boundingBoxOptFromProto(bbOpt: Option[ProtoBoundingBox]): Option[BoundingBox] = + implicit def boundingBoxOptFromProto(bbOpt: Option[BoundingBoxProto]): Option[BoundingBox] = bbOpt.map(bb => BoundingBox(bb.topLeft, bb.width, bb.height, bb.depth)) - implicit def elementClassToProto(ec: ElementClass.Value): ProtoElementClass = - ProtoElementClass.fromValue(ElementClass.bytesPerElement(ec)) + implicit def elementClassToProto(ec: ElementClass.Value): ElementClassProto = + ElementClassProto.fromValue(ElementClass.bytesPerElement(ec)) - implicit def elementClassFromProto(ec: ProtoElementClass): ElementClass.Value = + implicit def elementClassFromProto(ec: ElementClassProto): ElementClass.Value = ElementClass.guessFromBytesPerElement(ec.value).getOrElse(ElementClass.uint32) } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/SkeletonElementDefaults.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/SkeletonElementDefaults.scala index 2d9887e5cd5..44cebec92a3 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/SkeletonElementDefaults.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/helpers/SkeletonElementDefaults.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.datastore.helpers -import com.scalableminds.util.geometry.{Point3D, Vector3D} +import com.scalableminds.util.geometry.{Vec3Int, Vec3Double} import com.scalableminds.webknossos.datastore.SkeletonTracing.{Node, SkeletonTracing} object SkeletonTracingDefaults extends ProtoGeometryImplicits { @@ -9,8 +9,8 @@ object SkeletonTracingDefaults extends ProtoGeometryImplicits { private def createdTimestamp = System.currentTimeMillis() private val boundingBox = None private val activeNodeId = None - val editPosition: Point3D = Point3D(0, 0, 0) - val editRotation: Vector3D = Vector3D() + val editPosition: Vec3Int = Vec3Int(0, 0, 0) + val editRotation: Vec3Double = Vec3Double(0, 0, 0) val zoomLevel: Double = 2.0 private val version = 0 private val userBoundingBox = None @@ -30,8 +30,8 @@ object SkeletonTracingDefaults extends ProtoGeometryImplicits { object NodeDefaults extends ProtoGeometryImplicits { val id: Int = 0 - val rotation: Vector3D = Vector3D() - val position: Point3D = Point3D(0, 0, 0) + val rotation: Vec3Double = Vec3Double(0, 0, 0) + val position: Vec3Int = Vec3Int(0, 0, 0) val radius: Float = 1.0f val viewport: Int = 1 val resolution: Int = 1 diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala index c35ee007af2..949ffa5475a 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.datastore.models -import com.scalableminds.util.geometry.{Point3D, Vector3D, Vector3I} +import com.scalableminds.util.geometry.{Vec3Double, Vec3Int} import com.scalableminds.webknossos.datastore.models.datasource.DataLayer import com.scalableminds.webknossos.datastore.models.requests.{Cuboid, DataServiceRequestSettings} import play.api.libs.json.{Json, OFormat} @@ -24,7 +24,7 @@ case class DataRequest( } case class WebKnossosDataRequest( - position: Point3D, + position: Vec3Int, zoomStep: Int, cubeSize: Int, fourBit: Option[Boolean], @@ -47,12 +47,12 @@ object WebKnossosDataRequest { } case class WebKnossosIsosurfaceRequest( - position: Point3D, + position: Vec3Int, zoomStep: Int, - cubeSize: Point3D, + cubeSize: Vec3Int, segmentId: Long, - voxelDimensions: Vector3I, - scale: Vector3D, + subsamplingStrides: Vec3Int, + scale: Vec3Double, mapping: Option[String] = None, mappingType: Option[String] = None ) { diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/ImageThumbnail.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/ImageThumbnail.scala index 44fe345469b..304e28514da 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/ImageThumbnail.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/ImageThumbnail.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.datastore.models -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.models.datasource.DataLayerLike import play.api.libs.json.{Json, OFormat} @@ -27,7 +27,7 @@ object ImageThumbnail { // Parameters that seem to be working good enough val center = if (centerX.isDefined && centerY.isDefined && centerZ.isDefined) - Point3D(centerX.get, centerY.get, centerZ.get) + Vec3Int(centerX.get, centerY.get, centerZ.get) else dataLayer.boundingBox.center val resolutionExponent = bestResolutionExponent(dataLayer, zoomOpt) val resolution = dataLayer.lookUpResolution(resolutionExponent, snapToClosest = true) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/Positions.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/Positions.scala index 65fef7d858e..e59c23de880 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/Positions.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/Positions.scala @@ -1,14 +1,20 @@ package com.scalableminds.webknossos.datastore.models import com.scalableminds.webknossos.datastore.models.datasource.DataLayer -import com.scalableminds.util.geometry.{BoundingBox, GenericPosition, Point3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import org.apache.commons.lang3.builder.HashCodeBuilder +trait GenericPosition { + def x: Int + def y: Int + def z: Int +} + class VoxelPosition( protected val globalX: Int, protected val globalY: Int, protected val globalZ: Int, - val resolution: Point3D + val resolution: Vec3Int ) extends GenericPosition { val x: Int = globalX / resolution.x @@ -44,7 +50,7 @@ case class BucketPosition( globalX: Int, globalY: Int, globalZ: Int, - resolution: Point3D + resolution: Vec3Int ) extends GenericPosition { val bucketLength: Int = DataLayer.bucketLength @@ -78,10 +84,12 @@ case class BucketPosition( BucketPosition(globalX, globalY, globalZ + (bucketLength * resolution.z), resolution) def toHighestResBoundingBox: BoundingBox = - new BoundingBox(Point3D(globalX, globalY, globalZ), - bucketLength * resolution.x, - bucketLength * resolution.y, - bucketLength * resolution.z) + new BoundingBox( + Vec3Int(topLeft.x * resolution.x, topLeft.y * resolution.y, topLeft.z * resolution.z), + bucketLength * resolution.x, + bucketLength * resolution.y, + bucketLength * resolution.z + ) override def toString: String = s"BucketPosition($globalX, $globalY, $globalZ, mag$resolution)" @@ -91,7 +99,7 @@ class CubePosition( protected val globalX: Int, protected val globalY: Int, protected val globalZ: Int, - val resolution: Point3D, + val resolution: Vec3Int, val cubeLength: Int ) extends GenericPosition { @@ -110,7 +118,7 @@ class CubePosition( } def toHighestResBoundingBox: BoundingBox = - new BoundingBox(Point3D(globalX, globalY, globalZ), + new BoundingBox(Vec3Int(globalX, globalY, globalZ), cubeLength * resolution.x, cubeLength * resolution.y, cubeLength * resolution.z) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala index 6be8094d72b..4a10e34262a 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala @@ -4,7 +4,7 @@ import com.scalableminds.util.enumeration.ExtendedEnumeration import com.scalableminds.webknossos.datastore.dataformats.wkw.{WKWDataLayer, WKWSegmentationLayer} import com.scalableminds.webknossos.datastore.dataformats.{BucketProvider, MappingProvider} import com.scalableminds.webknossos.datastore.models.BucketPosition -import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration import play.api.libs.json._ @@ -74,13 +74,13 @@ trait DataLayerLike { def boundingBox: BoundingBox - def resolutions: List[Point3D] + def resolutions: List[Vec3Int] - def lookUpResolution(resolutionExponent: Int, snapToClosest: Boolean = false): Point3D = { + def lookUpResolution(resolutionExponent: Int, snapToClosest: Boolean = false): Vec3Int = { val resPower = Math.pow(2, resolutionExponent).toInt val matchOpt = resolutions.find(resolution => resolution.maxDim == resPower) if (snapToClosest) matchOpt.getOrElse(resolutions.minBy(resolution => math.abs(resPower - resolution.maxDim))) - else matchOpt.getOrElse(Point3D(resPower, resPower, resPower)) + else matchOpt.getOrElse(Vec3Int(resPower, resPower, resPower)) } def elementClass: ElementClass.Value @@ -124,11 +124,11 @@ trait DataLayer extends DataLayerLike { /** * Defines the length of the underlying cubes making up the layer. This is the maximal size that can be loaded from a single file. */ - def lengthOfUnderlyingCubes(resolution: Point3D): Int + def lengthOfUnderlyingCubes(resolution: Vec3Int): Int def bucketProvider: BucketProvider - def containsResolution(resolution: Point3D): Boolean = resolutions.contains(resolution) + def containsResolution(resolution: Vec3Int): Boolean = resolutions.contains(resolution) def doesContainBucket(bucket: BucketPosition): Boolean = boundingBox.intersects(bucket.toHighestResBoundingBox) @@ -184,7 +184,7 @@ case class AbstractDataLayer( name: String, category: Category.Value, boundingBox: BoundingBox, - resolutions: List[Point3D], + resolutions: List[Vec3Int], elementClass: ElementClass.Value, defaultViewConfiguration: Option[LayerViewConfiguration] = None, adminViewConfiguration: Option[LayerViewConfiguration] = None @@ -210,7 +210,7 @@ case class AbstractSegmentationLayer( name: String, category: Category.Value, boundingBox: BoundingBox, - resolutions: List[Point3D], + resolutions: List[Vec3Int], elementClass: ElementClass.Value, largestSegmentId: Long, mappings: Option[Set[String]], @@ -238,14 +238,14 @@ object AbstractSegmentationLayer { trait ResolutionFormatHelper { - implicit object resolutionFormat extends Format[Either[Int, Point3D]] { + implicit object resolutionFormat extends Format[Either[Int, Vec3Int]] { - override def reads(json: JsValue): JsResult[Either[Int, Point3D]] = - json.validate[Int].map[Either[Int, Point3D]](Left(_)).orElse(json.validate[Point3D].map(Right(_))) + override def reads(json: JsValue): JsResult[Either[Int, Vec3Int]] = + json.validate[Int].map[Either[Int, Vec3Int]](Left(_)).orElse(json.validate[Vec3Int].map(Right(_))) - override def writes(resolution: Either[Int, Point3D]): JsValue = resolution match { + override def writes(resolution: Either[Int, Vec3Int]): JsValue = resolution match { case Left(r) => JsNumber(r) - case Right(r) => Point3D.Point3DWrites.writes(r) + case Right(r) => Vec3Int.Vec3IntWrites.writes(r) } } } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataSource.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataSource.scala index 4273b5089e3..98f9f5d26d0 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataSource.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataSource.scala @@ -1,7 +1,7 @@ package com.scalableminds.webknossos.datastore.models import com.github.ghik.silencer.silent -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Scale} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration import com.scalableminds.webknossos.datastore.models.datasource.inbox.GenericInboxDataSource import play.api.libs.json._ @@ -21,20 +21,20 @@ package object datasource { case class GenericDataSource[+T <: DataLayerLike](id: DataSourceId, dataLayers: List[T], - scale: Scale, + scale: Vec3Double, defaultViewConfiguration: Option[DataSetViewConfiguration] = None) extends GenericInboxDataSource[T] { val toUsable: Option[GenericDataSource[T]] = Some(this) - val scaleOpt: Option[Scale] = Some(scale) + val scaleOpt: Option[Vec3Double] = Some(scale) val statusOpt: Option[String] = None def getDataLayer(name: String): Option[T] = dataLayers.find(_.name == name) - val center: Point3D = boundingBox.center + val center: Vec3Int = boundingBox.center lazy val boundingBox: BoundingBox = BoundingBox.combine(dataLayers.map(_.boundingBox)) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/InboxDataSource.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/InboxDataSource.scala index b122cde80b9..f3433a0539b 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/InboxDataSource.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/InboxDataSource.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.datastore.models.datasource -import com.scalableminds.util.geometry.Scale +import com.scalableminds.util.geometry.Vec3Double import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration import play.api.libs.json.{Format, JsResult, JsValue, Json} @@ -14,7 +14,7 @@ package object inbox { def isUsable: Boolean = toUsable.isDefined - def scaleOpt: Option[Scale] + def scaleOpt: Option[Vec3Double] def statusOpt: Option[String] @@ -37,12 +37,12 @@ package object inbox { case class UnusableDataSource[+T <: DataLayerLike](id: DataSourceId, status: String, - scale: Option[Scale] = None, + scale: Option[Vec3Double] = None, existingDataSourceProperties: Option[JsValue] = None) extends GenericInboxDataSource[T] { val toUsable: Option[GenericDataSource[T]] = None - val scaleOpt: Option[Scale] = scale + val scaleOpt: Option[Vec3Double] = scale val statusOpt: Option[String] = Some(status) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/Cuboid.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/Cuboid.scala index 157921858c4..57272f2eedc 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/Cuboid.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/Cuboid.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.datastore.models.requests -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.models.{BucketPosition, VoxelPosition} /** @@ -43,5 +43,5 @@ case class Cuboid(topLeft: VoxelPosition, width: Int, height: Int, depth: Int) { bucketList } - def resolution: Point3D = topLeft.resolution + def resolution: Vec3Int = topLeft.resolution } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/DataServiceRequests.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/DataServiceRequests.scala index ce35e6aae5a..9538778b764 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/DataServiceRequests.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/requests/DataServiceRequests.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.datastore.models.requests -import com.scalableminds.util.geometry.Vector3I +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.models.{BucketPosition, CubePosition} import com.scalableminds.webknossos.datastore.models.datasource.{DataLayer, DataSource, SegmentationLayer} import java.nio.file.Path @@ -19,7 +19,7 @@ case class DataServiceDataRequest( dataLayerMapping: Option[String], cuboid: Cuboid, settings: DataServiceRequestSettings, - voxelDimensions: Vector3I = Vector3I(1, 1, 1) + subsamplingStrides: Vec3Int = Vec3Int(1, 1, 1) // if > 1, skip voxels when loading (used for isosurface generation) ) case class DataReadInstruction( diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AgglomerateService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AgglomerateService.scala index 9e96714712e..21a0f954e31 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AgglomerateService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AgglomerateService.scala @@ -7,7 +7,7 @@ import ch.systemsx.cisd.hdf5._ import com.scalableminds.util.io.PathUtils import com.scalableminds.webknossos.datastore.DataStoreConfig import com.scalableminds.webknossos.datastore.SkeletonTracing.{Edge, SkeletonTracing, Tree} -import com.scalableminds.webknossos.datastore.geometry.Point3D +import com.scalableminds.webknossos.datastore.geometry.Vec3IntProto import com.scalableminds.webknossos.datastore.helpers.{NodeDefaults, SkeletonTracingDefaults} import com.scalableminds.webknossos.datastore.models.requests.DataServiceDataRequest import com.scalableminds.webknossos.datastore.storage._ @@ -157,7 +157,7 @@ class AgglomerateService @Inject()(config: DataStoreConfig) extends DataConverte case (pos, idx) => NodeDefaults.createInstance.copy( id = idx, - position = Point3D(pos(0).toInt, pos(1).toInt, pos(2).toInt) + position = Vec3IntProto(pos(0).toInt, pos(1).toInt, pos(2).toInt) ) } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala index 2964d6ae902..a739177363c 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/BinaryDataService.scala @@ -2,7 +2,7 @@ package com.scalableminds.webknossos.datastore.services import java.nio.file.Path -import com.scalableminds.util.geometry.{Point3D, Vector3I} +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.tools.ExtendedTypes.ExtendedArraySeq import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.helpers.DataSetDeleter @@ -29,7 +29,7 @@ class BinaryDataService(val dataBaseDir: Path, maxCacheSize: Int, val agglomerat if (!request.cuboid.hasValidDimensions) { Fox.failure("Invalid cuboid dimensions (must be > 0 and <= 512).") - } else if (request.cuboid.isSingleBucket(DataLayer.bucketLength) && request.voxelDimensions == Vector3I(1, 1, 1)) { + } else if (request.cuboid.isSingleBucket(DataLayer.bucketLength) && request.subsamplingStrides == Vec3Int(1, 1, 1)) { bucketQueue.headOption.toFox.flatMap { bucket => handleBucketRequest(request, bucket) } @@ -90,53 +90,53 @@ class BinaryDataService(val dataBaseDir: Path, maxCacheSize: Int, val agglomerat private def cutOutCuboid(request: DataServiceDataRequest, rs: List[(BucketPosition, Array[Byte])]): Array[Byte] = { val bytesPerElement = request.dataLayer.bytesPerElement val cuboid = request.cuboid - val voxelDimensions = request.voxelDimensions + val subsamplingStrides = request.subsamplingStrides - val resultVolume = Point3D( - math.ceil(cuboid.width.toDouble / voxelDimensions.x.toDouble).toInt, - math.ceil(cuboid.height.toDouble / voxelDimensions.y.toDouble).toInt, - math.ceil(cuboid.depth.toDouble / voxelDimensions.z.toDouble).toInt + val resultVolume = Vec3Int( + math.ceil(cuboid.width.toDouble / subsamplingStrides.x.toDouble).toInt, + math.ceil(cuboid.height.toDouble / subsamplingStrides.y.toDouble).toInt, + math.ceil(cuboid.depth.toDouble / subsamplingStrides.z.toDouble).toInt ) val result = new Array[Byte](resultVolume.x * resultVolume.y * resultVolume.z * bytesPerElement) val bucketLength = DataLayer.bucketLength rs.reverse.foreach { case (bucket, data) => - val xRemainder = cuboid.topLeft.x % voxelDimensions.x - val yRemainder = cuboid.topLeft.y % voxelDimensions.y - val zRemainder = cuboid.topLeft.z % voxelDimensions.z + val xRemainder = cuboid.topLeft.x % subsamplingStrides.x + val yRemainder = cuboid.topLeft.y % subsamplingStrides.y + val zRemainder = cuboid.topLeft.z % subsamplingStrides.z val xMin = math - .ceil((math.max(cuboid.topLeft.x, bucket.topLeft.x).toDouble - xRemainder) / voxelDimensions.x.toDouble) - .toInt * voxelDimensions.x + xRemainder + .ceil((math.max(cuboid.topLeft.x, bucket.topLeft.x).toDouble - xRemainder) / subsamplingStrides.x.toDouble) + .toInt * subsamplingStrides.x + xRemainder val yMin = math - .ceil((math.max(cuboid.topLeft.y, bucket.topLeft.y).toDouble - yRemainder) / voxelDimensions.y.toDouble) - .toInt * voxelDimensions.y + yRemainder + .ceil((math.max(cuboid.topLeft.y, bucket.topLeft.y).toDouble - yRemainder) / subsamplingStrides.y.toDouble) + .toInt * subsamplingStrides.y + yRemainder val zMin = math - .ceil((math.max(cuboid.topLeft.z, bucket.topLeft.z).toDouble - zRemainder) / voxelDimensions.z.toDouble) - .toInt * voxelDimensions.z + zRemainder + .ceil((math.max(cuboid.topLeft.z, bucket.topLeft.z).toDouble - zRemainder) / subsamplingStrides.z.toDouble) + .toInt * subsamplingStrides.z + zRemainder val xMax = math.min(cuboid.bottomRight.x, bucket.topLeft.x + bucketLength) val yMax = math.min(cuboid.bottomRight.y, bucket.topLeft.y + bucketLength) val zMax = math.min(cuboid.bottomRight.z, bucket.topLeft.z + bucketLength) for { - z <- zMin until zMax by voxelDimensions.z - y <- yMin until yMax by voxelDimensions.y - // if voxelDimensions.x == 1, we can bulk copy a row of voxels and do not need to iterate in the x dimension - x <- xMin until xMax by (if (voxelDimensions.x == 1) xMax else voxelDimensions.x) + z <- zMin until zMax by subsamplingStrides.z + y <- yMin until yMax by subsamplingStrides.y + // if subsamplingStrides.x == 1, we can bulk copy a row of voxels and do not need to iterate in the x dimension + x <- xMin until xMax by (if (subsamplingStrides.x == 1) xMax else subsamplingStrides.x) } { val dataOffset = (x % bucketLength + y % bucketLength * bucketLength + z % bucketLength * bucketLength * bucketLength) * bytesPerElement - val rx = (x - cuboid.topLeft.x) / voxelDimensions.x - val ry = (y - cuboid.topLeft.y) / voxelDimensions.y - val rz = (z - cuboid.topLeft.z) / voxelDimensions.z + val rx = (x - cuboid.topLeft.x) / subsamplingStrides.x + val ry = (y - cuboid.topLeft.y) / subsamplingStrides.y + val rz = (z - cuboid.topLeft.z) / subsamplingStrides.z val resultOffset = (rx + ry * resultVolume.x + rz * resultVolume.x * resultVolume.y) * bytesPerElement - if (voxelDimensions.x == 1) { + if (subsamplingStrides.x == 1) { // bulk copy a row of voxels System.arraycopy(data, dataOffset, result, resultOffset, (xMax - x) * bytesPerElement) } else { diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataFinder.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataFinder.scala index 49832ea20b2..bd483eb185d 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataFinder.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataFinder.scala @@ -1,10 +1,10 @@ package com.scalableminds.webknossos.datastore.services -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.models.datasource.DataLayer trait DataFinder { - private def getExactDataOffset(data: Array[Byte], bytesPerElement: Int): Point3D = { + private def getExactDataOffset(data: Array[Byte], bytesPerElement: Int): Vec3Int = { val bucketLength = DataLayer.bucketLength for { z <- 0 until bucketLength @@ -15,14 +15,14 @@ trait DataFinder { scaledZ = z * bytesPerElement * bucketLength * bucketLength } { val voxelOffset = scaledX + scaledY + scaledZ - if (data.slice(voxelOffset, voxelOffset + bytesPerElement).exists(_ != 0)) return Point3D(x, y, z) + if (data.slice(voxelOffset, voxelOffset + bytesPerElement).exists(_ != 0)) return Vec3Int(x, y, z) } - Point3D(0, 0, 0) + Vec3Int(0, 0, 0) } def getPositionOfNonZeroData(data: Array[Byte], - globalPositionOffset: Point3D, - bytesPerElement: Int): Option[Point3D] = + globalPositionOffset: Vec3Int, + bytesPerElement: Int): Option[Vec3Int] = if (data.nonEmpty && data.exists(_ != 0)) Some(globalPositionOffset.move(getExactDataOffset(data, bytesPerElement))) else None } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataSourceImporter.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataSourceImporter.scala index aed723c84b6..f3e88bb2fcc 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataSourceImporter.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataSourceImporter.scala @@ -1,13 +1,14 @@ package com.scalableminds.webknossos.datastore.services -import com.scalableminds.util.geometry.{Point3D, Scale} +import java.nio.file.Path + +import com.scalableminds.util.geometry.{Vec3Double, Vec3Int} import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.ExtendedTypes._ import com.scalableminds.webknossos.datastore.dataformats.MappingProvider import com.scalableminds.webknossos.datastore.models.datasource._ import net.liftweb.common.Box -import java.nio.file.Path import scala.collection.mutable.ArrayBuffer case class DataSourceImportReport[A](ctx: A, messages: ArrayBuffer[(String, String)] = ArrayBuffer.empty) { @@ -40,7 +41,7 @@ trait DataSourceImporter { } GenericDataSource(id, layers, - previous.map(_.scale).getOrElse(Scale.default), + previous.map(_.scale).getOrElse(Vec3Double(0, 0, 0)), previous.flatMap(_.defaultViewConfiguration)) } @@ -60,13 +61,13 @@ trait DataSourceImporter { } } - protected def parseResolutionName(path: Path): Option[Either[Int, Point3D]] = + protected def parseResolutionName(path: Path): Option[Either[Int, Vec3Int]] = path.getFileName.toString.toIntOpt match { case Some(resolutionInt) => Some(Left(resolutionInt)) case None => val pattern = """(\d+)-(\d+)-(\d+)""".r path.getFileName.toString match { - case pattern(x, y, z) => Some(Right(Point3D(x.toInt, y.toInt, z.toInt))) + case pattern(x, y, z) => Some(Right(Vec3Int(x.toInt, y.toInt, z.toInt))) case _ => None } } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataSourceService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataSourceService.scala index 87a43a06199..5a25fb7b794 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataSourceService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/DataSourceService.scala @@ -126,7 +126,7 @@ class DataSourceService @Inject()( val resolutionsByZ = dataSource.dataLayers.map(_.resolutions.sortBy(_.z)) val errors = List( - Check(dataSource.scale.isValid, "DataSource scale is invalid"), + Check(dataSource.scale.isStrictlyPositive, "DataSource scale is invalid"), Check(resolutionsByX == resolutionsByY && resolutionsByX == resolutionsByZ, "Scales do not monotonically increase in all dimensions"), Check(dataSource.dataLayers.nonEmpty, "DataSource must have at least one dataLayer"), diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/FindDataService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/FindDataService.scala index 581340cd827..d1761abd8fd 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/FindDataService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/FindDataService.scala @@ -1,7 +1,7 @@ package com.scalableminds.webknossos.datastore.services import com.google.inject.Inject -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.tools.{Fox, FoxImplicits, Math} import com.scalableminds.webknossos.datastore.models.datasource.{DataLayer, DataSource, ElementClass} import com.scalableminds.webknossos.datastore.models.requests.DataServiceDataRequest @@ -24,8 +24,8 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp private def getDataFor(dataSource: DataSource, dataLayer: DataLayer, - position: Point3D, - resolution: Point3D): Fox[Array[Byte]] = { + position: Vec3Int, + resolution: Vec3Int): Fox[Array[Byte]] = { val request = DataRequest( new VoxelPosition(position.x, position.y, position.z, resolution), DataLayer.bucketLength, @@ -45,8 +45,8 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp private def getConcatenatedDataFor(dataSource: DataSource, dataLayer: DataLayer, - positions: List[Point3D], - resolution: Point3D) = + positions: List[Vec3Int], + resolution: Vec3Int) = for { dataBucketWise: Seq[Array[Byte]] <- Fox .sequenceOfFulls(positions.map(getDataFor(dataSource, dataLayer, _, resolution))) @@ -58,7 +58,7 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp private def createPositions(dataLayer: DataLayer, iterationCount: Int = 4) = { @tailrec - def positionCreationIter(remainingRuns: List[Int], currentPositions: List[Point3D]): List[Point3D] = { + def positionCreationIter(remainingRuns: List[Int], currentPositions: List[Vec3Int]): List[Vec3Int] = { def createPositionsFromExponent(exponent: Int) = { val power = math.pow(2, exponent).toInt @@ -76,7 +76,7 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp y <- 1 until power x <- 1 until power } yield - Point3D(topLeft.x + x * spaceBetweenWidth, + Vec3Int(topLeft.x + x * spaceBetweenWidth, topLeft.y + y * spaceBetweenHeight, topLeft.z + z * spaceBetweenDepth)).toList ) @@ -93,13 +93,13 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp } } - positionCreationIter((1 to iterationCount).toList, List[Point3D]()) :+ dataLayer.boundingBox.topLeft + positionCreationIter((1 to iterationCount).toList, List[Vec3Int]()) :+ dataLayer.boundingBox.topLeft } private def checkAllPositionsForData(dataSource: DataSource, - dataLayer: DataLayer): Fox[Option[(Point3D, Point3D)]] = { + dataLayer: DataLayer): Fox[Option[(Vec3Int, Vec3Int)]] = { - def searchPositionIter(positions: List[Point3D], resolution: Point3D): Fox[Option[Point3D]] = + def searchPositionIter(positions: List[Vec3Int], resolution: Vec3Int): Fox[Option[Vec3Int]] = positions match { case List() => Fox.successful(None) case head :: tail => @@ -109,13 +109,13 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp } } - def checkIfPositionHasData(position: Point3D, resolution: Point3D) = + def checkIfPositionHasData(position: Vec3Int, resolution: Vec3Int) = for { data <- getDataFor(dataSource, dataLayer, position, resolution) position <- getPositionOfNonZeroData(data, position, dataLayer.bytesPerElement) } yield position - def resolutionIter(positions: List[Point3D], remainingResolutions: List[Point3D]): Fox[Option[(Point3D, Point3D)]] = + def resolutionIter(positions: List[Vec3Int], remainingResolutions: List[Vec3Int]): Fox[Option[(Vec3Int, Vec3Int)]] = remainingResolutions match { case List() => Fox.successful(None) case head :: tail => @@ -131,7 +131,7 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp resolutionIter(createPositions(dataLayer).distinct, dataLayer.resolutions.sortBy(_.maxDim)) } - def findPositionWithData(dataSource: DataSource, dataLayer: DataLayer): Fox[Option[(Point3D, Point3D)]] = + def findPositionWithData(dataSource: DataSource, dataLayer: DataLayer): Fox[Option[(Vec3Int, Vec3Int)]] = for { positionAndResolutionOpt <- checkAllPositionsForData(dataSource, dataLayer) } yield positionAndResolutionOpt @@ -147,7 +147,7 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp case d: Array[Float] => d.map(_.toDouble) } - def meanAndStdDevForPositions(positions: List[Point3D], resolution: Point3D): Fox[(Double, Double)] = + def meanAndStdDevForPositions(positions: List[Vec3Int], resolution: Vec3Int): Fox[(Double, Double)] = for { dataConcatenated <- getConcatenatedDataFor(dataSource, dataLayer, positions, resolution) dataAsDoubles = convertNonZeroDataToDouble(dataConcatenated, dataLayer.elementClass) @@ -204,7 +204,7 @@ class FindDataService @Inject()(dataServicesHolder: BinaryDataServiceHolder)(imp List(Histogram(counts, data.length, extrema._1, extrema._2)) } - def histogramForPositions(positions: List[Point3D], resolution: Point3D) = + def histogramForPositions(positions: List[Vec3Int], resolution: Vec3Int) = for { dataConcatenated <- getConcatenatedDataFor(dataSource, dataLayer, positions, resolution) ?~> "dataSet.noData" isUint24 = dataLayer.elementClass == ElementClass.uint24 diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/IsosurfaceService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/IsosurfaceService.scala index cdabb194f0c..ab71643c2f0 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/IsosurfaceService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/IsosurfaceService.scala @@ -4,7 +4,7 @@ import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.pattern.ask import akka.routing.RoundRobinPool import akka.util.Timeout -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Vector3D, Vector3I} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int, Vec3Double} import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.models.datasource.{DataSource, ElementClass, SegmentationLayer} import com.scalableminds.webknossos.datastore.models.requests.{ @@ -27,8 +27,8 @@ case class IsosurfaceRequest( dataLayer: SegmentationLayer, cuboid: Cuboid, segmentId: Long, - voxelDimensions: Vector3I, - scale: Vector3D, + subsamplingStrides: Vec3Int, + scale: Vec3Double, mapping: Option[String] = None, mappingType: Option[String] = None ) @@ -115,7 +115,7 @@ class IsosurfaceService(binaryDataService: BinaryDataService, request.mapping, request.cuboid, DataServiceRequestSettings(halfByte = false, request.mapping, None), - request.voxelDimensions + request.subsamplingStrides ) agglomerateService.applyAgglomerate(dataRequest)(data) case _ => @@ -134,7 +134,7 @@ class IsosurfaceService(binaryDataService: BinaryDataService, } def subVolumeContainsSegmentId[T](data: Array[T], - dataDimensions: Vector3I, + dataDimensions: Vec3Int, boundingBox: BoundingBox, segmentId: T): Boolean = { for { @@ -148,16 +148,16 @@ class IsosurfaceService(binaryDataService: BinaryDataService, false } - def findNeighbors[T](data: Array[T], dataDimensions: Vector3I, segmentId: T): List[Int] = { + def findNeighbors[T](data: Array[T], dataDimensions: Vec3Int, segmentId: T): List[Int] = { val x = dataDimensions.x - 1 val y = dataDimensions.y - 1 val z = dataDimensions.z - 1 - val front_xy = BoundingBox(Point3D(0, 0, 0), x, y, 1) - val front_xz = BoundingBox(Point3D(0, 0, 0), x, 1, z) - val front_yz = BoundingBox(Point3D(0, 0, 0), 1, y, z) - val back_xy = BoundingBox(Point3D(0, 0, z), x, y, 1) - val back_xz = BoundingBox(Point3D(0, y, 0), x, 1, z) - val back_yz = BoundingBox(Point3D(x, 0, 0), 1, y, z) + val front_xy = BoundingBox(Vec3Int(0, 0, 0), x, y, 1) + val front_xz = BoundingBox(Vec3Int(0, 0, 0), x, 1, z) + val front_yz = BoundingBox(Vec3Int(0, 0, 0), 1, y, z) + val back_xy = BoundingBox(Vec3Int(0, 0, z), x, y, 1) + val back_xz = BoundingBox(Vec3Int(0, y, 0), x, 1, z) + val back_yz = BoundingBox(Vec3Int(x, 0, 0), 1, y, z) val surfaceBoundingBoxes = List(front_xy, front_xz, front_yz, back_xy, back_xz, back_yz) surfaceBoundingBoxes.zipWithIndex.filter { case (surfaceBoundingBox, index) => @@ -168,24 +168,27 @@ class IsosurfaceService(binaryDataService: BinaryDataService, } val cuboid = request.cuboid - val voxelDimensions = Vector3D(request.voxelDimensions.x, request.voxelDimensions.y, request.voxelDimensions.z) + val subsamplingStrides = + Vec3Double(request.subsamplingStrides.x, request.subsamplingStrides.y, request.subsamplingStrides.z) val dataRequest = DataServiceDataRequest(request.dataSource.orNull, request.dataLayer, request.mapping, cuboid, DataServiceRequestSettings.default, - request.voxelDimensions) + request.subsamplingStrides) - val dataDimensions = Vector3I(math.ceil(cuboid.width / voxelDimensions.x).toInt, - math.ceil(cuboid.height / voxelDimensions.y).toInt, - math.ceil(cuboid.depth / voxelDimensions.z).toInt) + val dataDimensions = Vec3Int( + math.ceil(cuboid.width / subsamplingStrides.x).toInt, + math.ceil(cuboid.height / subsamplingStrides.y).toInt, + math.ceil(cuboid.depth / subsamplingStrides.z).toInt + ) - val offset = Vector3D(cuboid.topLeft.x, cuboid.topLeft.y, cuboid.topLeft.z) - val scale = Vector3D(cuboid.topLeft.resolution) * request.scale + val offset = Vec3Double(cuboid.topLeft.x, cuboid.topLeft.y, cuboid.topLeft.z) + val scale = Vec3Double(cuboid.topLeft.resolution) * request.scale val typedSegmentId = dataTypeFunctors.fromLong(request.segmentId) - val vertexBuffer = mutable.ArrayBuffer[Vector3D]() + val vertexBuffer = mutable.ArrayBuffer[Vec3Double]() for { data <- binaryDataService.handleDataRequest(dataRequest) @@ -200,7 +203,7 @@ class IsosurfaceService(binaryDataService: BinaryDataService, y <- 0 until dataDimensions.y by 32 z <- 0 until dataDimensions.z by 32 } { - val boundingBox = BoundingBox(Point3D(x, y, z), + val boundingBox = BoundingBox(Vec3Int(x, y, z), math.min(dataDimensions.x - x, 33), math.min(dataDimensions.y - y, 33), math.min(dataDimensions.z - z, 33)) @@ -209,7 +212,7 @@ class IsosurfaceService(binaryDataService: BinaryDataService, dataDimensions, boundingBox, mappedSegmentId, - voxelDimensions, + subsamplingStrides, offset, scale, vertexBuffer) 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 dbf51069682..7e8b164521a 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/MeshFileService.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.datastore.services import java.nio.file.{Path, Paths} import ch.systemsx.cisd.hdf5.HDF5FactoryProvider -import com.scalableminds.util.geometry.Point3D +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 @@ -31,7 +31,7 @@ object ListMeshChunksRequest { case class MeshChunkDataRequest( meshFile: String, - position: Point3D, + position: Vec3Int, segmentId: Long ) @@ -97,7 +97,7 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC def listMeshChunksForSegment(organizationName: String, dataSetName: String, dataLayerName: String, - listMeshChunksRequest: ListMeshChunksRequest): Fox[List[Point3D]] = { + listMeshChunksRequest: ListMeshChunksRequest): Fox[List[Vec3Int]] = { val meshFilePath = dataBaseDir .resolve(organizationName) @@ -145,15 +145,15 @@ class MeshFileService @Inject()(config: DataStoreConfig)(implicit ec: ExecutionC } yield (data, encoding) } - private def positionLiteral(position: Point3D) = + private def positionLiteral(position: Vec3Int) = s"${position.x}_${position.y}_${position.z}" - private def parsePositionLiteral(positionLiteral: String): Fox[Point3D] = { + private def parsePositionLiteral(positionLiteral: String): Fox[Vec3Int] = { val split = positionLiteral.split("_").toList for { _ <- bool2Fox(split.length == 3) asInts <- tryo { split.map(_.toInt) } - } yield Point3D(asInts.head, asInts(1), asInts(2)) + } yield Vec3Int(asInts.head, asInts(1), asInts(2)) } def initHDFReader(meshFilePath: Path): CachedMeshFile = { diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/UploadService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/UploadService.scala index 97b5ede0132..92d5daad7bf 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/UploadService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/UploadService.scala @@ -217,7 +217,7 @@ class UploadService @Inject()(dataSourceRepository: DataSourceRepository, private def postProcessUploadedDataSource(datasetNeedsConversion: Boolean, unpackToDir: Path, dataSourceId: DataSourceId, - layersToLink: Option[List[LinkedLayerIdentifier]]) = + layersToLink: Option[List[LinkedLayerIdentifier]]): Fox[Unit] = if (datasetNeedsConversion) Fox.successful(()) else { @@ -241,9 +241,10 @@ class UploadService @Inject()(dataSourceRepository: DataSourceRepository, case Failure(msg, e, _) => deleteOnDisk(dataSourceId.team, dataSourceId.name, dataSetNeedsConversion, Some("the upload failed")) dataSourceRepository.cleanUpDataSource(dataSourceId) - val errorMsg = s"Error $label: $msg, $e" - logger.warn(errorMsg) - Fox.failure(errorMsg) + logger.warn(s"Error while $label: $msg, $e") + for { + _ <- result ?~> f"Error while $label" + } yield () } private def ensureAllChunksUploaded(uploadId: String): Fox[Unit] = diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mcubes/MarchingCubes.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mcubes/MarchingCubes.scala index faa934da800..f68c06d5800 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mcubes/MarchingCubes.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mcubes/MarchingCubes.scala @@ -1,19 +1,19 @@ package com.scalableminds.webknossos.datastore.services.mcubes -import com.scalableminds.util.geometry.{BoundingBox, Vector3D, Vector3I} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} import scala.collection.mutable object MarchingCubes { def marchingCubes[T](data: Array[T], - dataDimensions: Vector3I, + dataDimensions: Vec3Int, boundingBox: BoundingBox, segmentId: T, - voxelDimensions: Vector3D, - offset: Vector3D, - scale: Vector3D, - vertexBuffer: mutable.ArrayBuffer[Vector3D]) { + subsamplingStrides: Vec3Double, + offset: Vec3Double, + scale: Vec3Double, + vertexBuffer: mutable.ArrayBuffer[Vec3Double]) { def getVoxelData(x: Int, y: Int, z: Int): T = data(x + (dataDimensions.x * y) + (dataDimensions.x * dataDimensions.y * z)) @@ -50,9 +50,9 @@ object MarchingCubes { if (getVoxelData(x, y + 1, z + 1) == segmentId) cubeIndex |= 128 if (getVoxelData(x + 1, y + 1, z + 1) == segmentId) cubeIndex |= 64 - val position = Vector3D(x, y, z) + val position = Vec3Double(x, y, z) MarchingCubesTable.triangleTable(cubeIndex).foreach { edgeDelta => - vertexBuffer += ((position + edgeDelta) * voxelDimensions + offset) * scale + vertexBuffer += ((position + edgeDelta) * subsamplingStrides + offset) * scale } } } diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mcubes/MarchingCubesTables.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mcubes/MarchingCubesTables.scala index 1167e3ba755..29926d1f275 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mcubes/MarchingCubesTables.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/mcubes/MarchingCubesTables.scala @@ -1,265 +1,265 @@ package com.scalableminds.webknossos.datastore.services.mcubes -import com.scalableminds.util.geometry.Vector3D +import com.scalableminds.util.geometry.Vec3Double object MarchingCubesTable { - val triangleTable: Array[Array[Vector3D]] = Array( + val triangleTable: Array[Array[Vec3Double]] = Array( Array(), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5)), - Array(Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5)), - Array(Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0)), - Array(Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0)), - Array(Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5)), - Array(Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1)), - Array(Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5)), - Array(Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0)), - Array(Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5)), - Array(Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0)), - Array(Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0)), - Array(Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5)), - Array(Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5)), - Array(Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0)), - Array(Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0)), - Array(Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0)), - Array(Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1)), - Array(Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1)), - Array(Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0)), - Array(Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1)), - Array(Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1)), - Array(Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0)), - Array(Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0)), - Array(Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1)), - Array(Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1)), - Array(Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(0.5, 1, 1), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5)), - Array(Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0)), - Array(Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1)), - Array(Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0)), - Array(Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1)), - Array(Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1)), - Array(Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0)), - Array(Vector3D(0.5, 1, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5)), - Array(Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1)), - Array(Vector3D(0.5, 1, 1), Vector3D(1, 0.5, 1), Vector3D(1, 1, 0.5)), - Array(Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0)), - Array(Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5)), - Array(Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0)), - Array(Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1)), - Array(Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0)), - Array(Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0)), - Array(Vector3D(1, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(1, 0, 0.5)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5)), - Array(Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0)), - Array(Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0)), - Array(Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 1), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0.5, 1), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(0.5, 1, 0)), - Array(Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(0.5, 0, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1)), - Array(Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5)), - Array(Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 1), Vector3D(0, 0.5, 1)), - Array(Vector3D(0, 0.5, 1), Vector3D(0, 0, 0.5), Vector3D(0.5, 0, 1)), - Array(Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5)), - Array(Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5)), - Array(Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0.5, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0.5, 0), Vector3D(0, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0.5, 0, 0), Vector3D(0, 1, 0.5), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5)), - Array(Vector3D(0, 1, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0.5, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(0, 0, 0.5), Vector3D(0.5, 1, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(0.5, 0, 0), Vector3D(0, 0, 0.5), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0)), - Array(Vector3D(0.5, 1, 0), Vector3D(1, 1, 0.5), Vector3D(1, 0.5, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(1, 0.5, 0), Vector3D(0, 0, 0.5), Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5)), - Array(Vector3D(1, 0.5, 0), Vector3D(1, 0, 0.5), Vector3D(0.5, 0, 0)), - Array(Vector3D(0, 0, 0.5), Vector3D(0, 0.5, 0), Vector3D(0.5, 0, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0.5, 1, 1), Vec3Double(1, 0.5, 1), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(1, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(0.5, 0, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 1), Vec3Double(0, 0.5, 1)), + Array(Vec3Double(0, 0.5, 1), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 0, 1)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0.5, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0.5, 0), Vec3Double(0, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 1, 0.5), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5)), + Array(Vec3Double(0, 1, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0.5, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(0, 0, 0.5), Vec3Double(0.5, 1, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(0.5, 0, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0.5, 1, 0), Vec3Double(1, 1, 0.5), Vec3Double(1, 0.5, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(1, 0.5, 0), Vec3Double(0, 0, 0.5), Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5)), + Array(Vec3Double(1, 0.5, 0), Vec3Double(1, 0, 0.5), Vec3Double(0.5, 0, 0)), + Array(Vec3Double(0, 0, 0.5), Vec3Double(0, 0.5, 0), Vec3Double(0.5, 0, 0)), Array() ) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/AgglomerateFileCache.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/AgglomerateFileCache.scala index a68d8685fed..76fac47d0f3 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/AgglomerateFileCache.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/AgglomerateFileCache.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.datastore.storage import java.util import ch.systemsx.cisd.hdf5.{HDF5DataSet, IHDF5Reader} import com.scalableminds.util.cache.LRUConcurrentCache -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.webknossos.datastore.dataformats.SafeCachable import com.scalableminds.webknossos.datastore.models.requests.{Cuboid, DataServiceDataRequest} import com.scalableminds.webknossos.datastore.storage @@ -112,7 +112,7 @@ class BoundingBoxCache( private def getGlobalCuboid(cuboid: Cuboid): Cuboid = { val res = cuboid.resolution val tl = cuboid.topLeft - Cuboid(new VoxelPosition(tl.x * res.x, tl.y * res.y, tl.z * res.z, Point3D(1, 1, 1)), + Cuboid(new VoxelPosition(tl.x * res.x, tl.y * res.y, tl.z * res.z, Vec3Int(1, 1, 1)), cuboid.width * res.x, cuboid.height * res.y, cuboid.depth * res.z) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/DataCubeCache.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/DataCubeCache.scala index 9b99d3f2651..aec1dc65474 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/DataCubeCache.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/storage/DataCubeCache.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.datastore.storage import com.scalableminds.webknossos.datastore.dataformats.wkw.WKWCube import com.scalableminds.webknossos.datastore.models.requests.DataReadInstruction import com.scalableminds.util.cache.LRUConcurrentCache -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.tools.{Fox, FoxImplicits} import net.liftweb.common.{Box, Empty, Failure, Full} @@ -13,7 +13,7 @@ case class CachedCube( organization: String, dataSourceName: String, dataLayerName: String, - resolution: Point3D, + resolution: Vec3Int, x: Int, y: Int, z: Int diff --git a/webknossos-datastore/proto/SkeletonTracing.proto b/webknossos-datastore/proto/SkeletonTracing.proto index 4ef46353b51..d1fdf7c7632 100644 --- a/webknossos-datastore/proto/SkeletonTracing.proto +++ b/webknossos-datastore/proto/SkeletonTracing.proto @@ -6,8 +6,8 @@ import "geometry.proto"; message Node { required int32 id = 1; - required Point3D position = 2; - required Vector3D rotation = 3; + required Vec3IntProto position = 2; + required Vec3DoubleProto rotation = 3; required float radius = 4; required int32 viewport = 5; required int32 resolution = 6; @@ -35,7 +35,7 @@ message Tree { required int32 treeId = 1; repeated Node nodes = 2; repeated Edge edges = 3; - optional Color color = 4; + optional ColorProto color = 4; repeated BranchPoint branchPoints = 5; repeated Comment comments = 6; required string name = 7; @@ -54,15 +54,15 @@ message SkeletonTracing { required string dataSetName = 1; repeated Tree trees = 2; required int64 createdTimestamp = 3; - optional BoundingBox boundingBox = 4; + optional BoundingBoxProto boundingBox = 4; optional int32 activeNodeId = 5; - required Point3D editPosition = 6; - required Vector3D editRotation = 7; + required Vec3IntProto editPosition = 6; + required Vec3DoubleProto editRotation = 7; required double zoomLevel = 8; required int64 version = 9; - optional BoundingBox userBoundingBox = 10; + optional BoundingBoxProto userBoundingBox = 10; repeated TreeGroup treeGroups = 11; - repeated NamedBoundingBox userBoundingBoxes = 12; + repeated NamedBoundingBoxProto userBoundingBoxes = 12; optional string organizationName = 13; // to identify the dataset (may differ from annotation orga) } diff --git a/webknossos-datastore/proto/VolumeTracing.proto b/webknossos-datastore/proto/VolumeTracing.proto index d01e51bd311..e8892623878 100644 --- a/webknossos-datastore/proto/VolumeTracing.proto +++ b/webknossos-datastore/proto/VolumeTracing.proto @@ -6,7 +6,7 @@ import "geometry.proto"; message Segment { required int64 segmentId = 1; - optional Point3D anchorPosition = 2; + optional Vec3IntProto anchorPosition = 2; optional string name = 3; optional int64 creationTime = 4; } @@ -21,20 +21,20 @@ message VolumeTracing { } optional int64 activeSegmentId = 1; - required BoundingBox boundingBox = 2; + required BoundingBoxProto boundingBox = 2; required int64 createdTimestamp = 3; required string dataSetName = 4; - required Point3D editPosition = 5; - required Vector3D editRotation = 6; + required Vec3IntProto editPosition = 5; + required Vec3DoubleProto editRotation = 6; required ElementClass elementClass = 7; optional string fallbackLayer = 8; required int64 largestSegmentId = 9; required int64 version = 10; required double zoomLevel = 11; - optional BoundingBox userBoundingBox = 12; - repeated NamedBoundingBox userBoundingBoxes = 13; + optional BoundingBoxProto userBoundingBox = 12; + repeated NamedBoundingBoxProto userBoundingBoxes = 13; optional string organizationName = 14; // to identify the dataset (may differ from annotation orga) - repeated Point3D resolutions = 15; + repeated Vec3IntProto resolutions = 15; repeated Segment segments = 16; } diff --git a/webknossos-datastore/proto/geometry.proto b/webknossos-datastore/proto/geometry.proto index 879bac351e7..ef77250e68c 100644 --- a/webknossos-datastore/proto/geometry.proto +++ b/webknossos-datastore/proto/geometry.proto @@ -3,36 +3,36 @@ syntax = "proto2"; package com.scalableminds.webknossos.datastore; -message Point3D { +message Vec3IntProto { required int32 x = 1; required int32 y = 2; required int32 z = 3; } -message Vector3D { +message Vec3DoubleProto { required double x = 1; required double y = 2; required double z = 3; } -message Color { +message ColorProto { required double r = 1; required double g = 2; required double b = 3; required double a = 4; } -message BoundingBox { - required Point3D topLeft = 1; +message BoundingBoxProto { + required Vec3IntProto topLeft = 1; required int32 width = 2; required int32 height = 3; required int32 depth = 4; } -message NamedBoundingBox { +message NamedBoundingBoxProto { required int32 id = 1; // Zero shouldn't be used as an id because it's reserved for the original user bounding box optional string name = 2; optional bool isVisible = 3; - optional Color color = 4; - required BoundingBox boundingBox = 5; + optional ColorProto color = 4; + required BoundingBoxProto boundingBox = 5; } diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala index 0ebc30d84bd..5ac52504fe8 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala @@ -5,7 +5,7 @@ import java.nio.{ByteBuffer, ByteOrder} import akka.stream.scaladsl.Source import com.google.inject.Inject -import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import com.scalableminds.util.tools.ExtendedTypes.ExtendedString import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.models.datasource.DataSourceLike @@ -253,7 +253,7 @@ class VolumeTracingController @Inject()(val tracingService: VolumeTracingService for { positionOpt <- tracingService.findData(tracingId) } yield { - Ok(Json.obj("position" -> positionOpt, "resolution" -> positionOpt.map(_ => Point3D(1, 1, 1)))) + Ok(Json.obj("position" -> positionOpt, "resolution" -> positionOpt.map(_ => Vec3Int(1, 1, 1)))) } } } diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/BoundingBoxMerger.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/BoundingBoxMerger.scala index 1130eeb57f4..c347aa67240 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/BoundingBoxMerger.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/BoundingBoxMerger.scala @@ -1,7 +1,7 @@ package com.scalableminds.webknossos.tracingstore.tracings -import com.scalableminds.webknossos.datastore.geometry.{NamedBoundingBox => ProtoNamedBoundingBox} -import com.scalableminds.webknossos.datastore.geometry.{BoundingBox => ProtoBoundingBox} +import com.scalableminds.webknossos.datastore.geometry.{NamedBoundingBoxProto => ProtoNamedBoundingBox} +import com.scalableminds.webknossos.datastore.geometry.{BoundingBoxProto => ProtoBoundingBox} import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits trait BoundingBoxMerger extends ProtoGeometryImplicits { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala index 2904616c67b..0d31b615de0 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/NamedBoundingBox.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.tracingstore.tracings import play.api.libs.json.{Json, OFormat} import com.scalableminds.util.geometry.BoundingBox import com.scalableminds.util.image.Color -import com.scalableminds.webknossos.datastore.geometry.{NamedBoundingBox => ProtoBoundingBox} +import com.scalableminds.webknossos.datastore.geometry.{NamedBoundingBoxProto => ProtoBoundingBox} import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits import com.scalableminds.webknossos.tracingstore.tracings.skeleton.updating.SkeletonUpdateActionHelper diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/TracingMigrationService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/TracingMigrationService.scala index 46fe4691839..23f689fcec8 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/TracingMigrationService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/TracingMigrationService.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.tracingstore.tracings import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.SkeletonTracing.SkeletonTracing import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing -import com.scalableminds.webknossos.datastore.geometry.{Color, NamedBoundingBox => ProtoBox} +import com.scalableminds.webknossos.datastore.geometry.{ColorProto, NamedBoundingBoxProto => ProtoBox} import net.liftweb.common.Full import scalapb.GeneratedMessage @@ -13,8 +13,8 @@ trait ColorGenerator { private def getRandomComponent: Double = Math.random() - def getRandomColor: Color = - Color(getRandomComponent, getRandomComponent, getRandomComponent, 1.0) + def getRandomColor: ColorProto = + ColorProto(getRandomComponent, getRandomComponent, getRandomComponent, 1.0) } trait TracingMigrationService[T <: GeneratedMessage] extends FoxImplicits { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/SkeletonTracingService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/SkeletonTracingService.scala index 7b5b3ae31de..036af9c7d38 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/SkeletonTracingService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/SkeletonTracingService.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.skeleton import com.google.inject.Inject import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.SkeletonTracing.SkeletonTracing -import com.scalableminds.webknossos.datastore.geometry.NamedBoundingBox +import com.scalableminds.webknossos.datastore.geometry.NamedBoundingBoxProto import com.scalableminds.webknossos.datastore.helpers.{ProtoGeometryImplicits, SkeletonTracingDefaults} import com.scalableminds.webknossos.tracingstore.TracingStoreRedisStore import com.scalableminds.webknossos.tracingstore.tracings.UpdateAction.SkeletonUpdateAction @@ -139,7 +139,7 @@ class SkeletonTracingService @Inject()( val taskBoundingBox = if (fromTask) { tracing.boundingBox.map { bb => val newId = if (tracing.userBoundingBoxes.isEmpty) 1 else tracing.userBoundingBoxes.map(_.id).max + 1 - NamedBoundingBox(newId, Some("task bounding box"), Some(true), Some(getRandomColor), bb) + NamedBoundingBoxProto(newId, Some("task bounding box"), Some(true), Some(getRandomColor), bb) } } else None diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActionHelper.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActionHelper.scala index d917e0800c5..08dc831c0b2 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActionHelper.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActionHelper.scala @@ -1,7 +1,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.skeleton.updating import com.scalableminds.webknossos.datastore.SkeletonTracing._ -import com.scalableminds.webknossos.datastore.geometry.Color +import com.scalableminds.webknossos.datastore.geometry.ColorProto trait SkeletonUpdateActionHelper { @@ -16,13 +16,13 @@ trait SkeletonUpdateActionHelper { .find(_.treeId == treeId) .getOrElse(throw new NoSuchElementException("Tracing does not contain tree with requested id " + treeId)) - protected def convertColor(aColor: com.scalableminds.util.image.Color): Color = - Color(aColor.r, aColor.g, aColor.b, aColor.a) + protected def convertColor(aColor: com.scalableminds.util.image.Color): ColorProto = + ColorProto(aColor.r, aColor.g, aColor.b, aColor.a) protected def convertBranchPoint(aBranchPoint: UpdateActionBranchPoint): BranchPoint = BranchPoint(aBranchPoint.nodeId, aBranchPoint.timestamp) protected def convertComment(aComment: UpdateActionComment): Comment = Comment(aComment.nodeId, aComment.content) - protected def convertColorOpt(aColorOpt: Option[com.scalableminds.util.image.Color]): Option[Color] = + protected def convertColorOpt(aColorOpt: Option[com.scalableminds.util.image.Color]): Option[ColorProto] = aColorOpt match { case Some(aColor) => Some(convertColor(aColor)) case None => None diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala index 30f91613303..c4b79dff8ba 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/skeleton/updating/SkeletonUpdateActions.scala @@ -3,7 +3,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.skeleton.updating import com.scalableminds.webknossos.datastore.SkeletonTracing._ import com.scalableminds.webknossos.tracingstore.tracings._ import com.scalableminds.webknossos.tracingstore.tracings.skeleton._ -import com.scalableminds.util.geometry.{Point3D, Vector3D} +import com.scalableminds.util.geometry.{Vec3Int, Vec3Double} import com.scalableminds.webknossos.datastore.helpers.{NodeDefaults, ProtoGeometryImplicits} import play.api.libs.json._ @@ -171,8 +171,8 @@ case class DeleteEdgeSkeletonAction(source: Int, } case class CreateNodeSkeletonAction(id: Int, - position: Point3D, - rotation: Option[Vector3D], + position: Vec3Int, + rotation: Option[Vec3Double], radius: Option[Float], viewport: Option[Int], resolution: Option[Int], @@ -210,8 +210,8 @@ case class CreateNodeSkeletonAction(id: Int, } case class UpdateNodeSkeletonAction(id: Int, - position: Point3D, - rotation: Option[Vector3D], + position: Vec3Int, + rotation: Option[Vec3Double], radius: Option[Float], viewport: Option[Int], resolution: Option[Int], @@ -284,8 +284,8 @@ case class UpdateTreeGroupsSkeletonAction(treeGroups: List[UpdateActionTreeGroup } case class UpdateTracingSkeletonAction(activeNode: Option[Int], - editPosition: com.scalableminds.util.geometry.Point3D, - editRotation: com.scalableminds.util.geometry.Vector3D, + editPosition: com.scalableminds.util.geometry.Vec3Int, + editRotation: com.scalableminds.util.geometry.Vec3Double, zoomLevel: Double, userBoundingBox: Option[com.scalableminds.util.geometry.BoundingBox], actionTimestamp: Option[Long] = None, diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/FallbackLayerAdapter.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/FallbackLayerAdapter.scala index c6c82c02e47..bb2368a5da5 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/FallbackLayerAdapter.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/FallbackLayerAdapter.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume -import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.dataformats.{BucketProvider, MappingProvider} import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration @@ -34,13 +34,13 @@ class FallbackLayerAdapter(primary: SegmentationLayer, fallback: SegmentationLay lazy val boundingBox: BoundingBox = primary.boundingBox - val resolutions: List[Point3D] = primary.resolutions.union(fallback.resolutions) + val resolutions: List[Vec3Int] = primary.resolutions.union(fallback.resolutions) val elementClass: ElementClass.Value = primary.elementClass val dataFormat: DataFormat.Value = DataFormat.tracing - def lengthOfUnderlyingCubes(resolution: Point3D): Int = fallback.lengthOfUnderlyingCubes(resolution) + def lengthOfUnderlyingCubes(resolution: Vec3Int): Int = fallback.lengthOfUnderlyingCubes(resolution) val largestSegmentId: Long = math.max(primary.largestSegmentId, fallback.largestSegmentId) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/MergedVolume.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/MergedVolume.scala index 6405bf5b482..5e601de323a 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/MergedVolume.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/MergedVolume.scala @@ -2,7 +2,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume import java.io.File -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.tools.{ByteUtils, Fox} import com.scalableminds.webknossos.datastore.models.{BucketPosition, UnsignedInteger, UnsignedIntegerArray} import com.scalableminds.webknossos.datastore.services.DataConverter @@ -38,7 +38,7 @@ class MergedVolume(elementClass: ElementClass, initialLargestSegmentId: Long = 0 } def addLabelSetFromBucketStream(bucketStream: Iterator[(BucketPosition, Array[Byte])], - allowedResolutions: Set[Point3D]): Unit = { + allowedResolutions: Set[Vec3Int]): Unit = { val labelSet: mutable.Set[UnsignedInteger] = scala.collection.mutable.Set() bucketStream.foreach { case (bucketPosition, data) => @@ -75,7 +75,7 @@ class MergedVolume(elementClass: ElementClass, initialLargestSegmentId: Long = 0 def addFromBucketStream(sourceVolumeIndex: Int, bucketStream: Iterator[(BucketPosition, Array[Byte])], - allowedResolutions: Option[Set[Point3D]] = None): Unit = + allowedResolutions: Option[Set[Vec3Int]] = None): Unit = bucketStream.foreach { case (bucketPosition, bytes) => if (!isAllZero(bytes) && allowedResolutions.forall(_.contains(bucketPosition.resolution))) { @@ -126,7 +126,7 @@ class MergedVolume(elementClass: ElementClass, initialLargestSegmentId: Long = 0 }.toList) } yield () - def presentResolutions: Set[Point3D] = + def presentResolutions: Set[Vec3Int] = mergedVolume.map { case (bucketPosition: BucketPosition, _) => bucketPosition.resolution }.toSet diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/Volume.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/Volume.scala deleted file mode 100644 index 41c5c4b7754..00000000000 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/Volume.scala +++ /dev/null @@ -1,3 +0,0 @@ -package com.scalableminds.webknossos.tracingstore.tracings.volume - -case class Volume(location: String, fallbackLayer: Option[String]) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeDataZipHelper.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeDataZipHelper.scala index c592b37e34b..b55373e02f9 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeDataZipHelper.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeDataZipHelper.scala @@ -2,7 +2,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume import java.io.{File, FileOutputStream, InputStream} -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.io.ZipIO import com.scalableminds.util.tools.ByteUtils import com.scalableminds.webknossos.datastore.dataformats.wkw.WKWDataFormatHelper @@ -35,8 +35,8 @@ trait VolumeDataZipHelper extends WKWDataFormatHelper with ByteUtils { unzipResult.map(_ => ()) } - protected def resolutionSetFromZipfile(zipFile: File): Set[Point3D] = { - val resolutionSet = new mutable.HashSet[Point3D]() + protected def resolutionSetFromZipfile(zipFile: File): Set[Vec3Int] = { + val resolutionSet = new mutable.HashSet[Vec3Int]() ZipIO.withUnziped(zipFile) { case (fileName, _) => parseWKWFilePath(fileName.toString).map { bucketPosition: BucketPosition => diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingBucketHelper.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingBucketHelper.scala index f3269479ce4..a48713783b0 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingBucketHelper.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingBucketHelper.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.dataformats.wkw.WKWDataFormatHelper import com.scalableminds.webknossos.datastore.models.BucketPosition @@ -74,7 +74,7 @@ trait BucketKeys extends WKWMortonHelper with WKWDataFormatHelper with LazyLoggi s"$dataLayerName/${formatResolution(bucket.resolution)}/$mortonIndex-[${bucket.x},${bucket.y},${bucket.z}]" } - protected def formatResolution(resolution: Point3D): String = + protected def formatResolution(resolution: Vec3Int): String = if (resolution.x == resolution.y && resolution.x == resolution.z) s"${resolution.maxDim}" else diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDefaults.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDefaults.scala index 75f11275b83..885210b317c 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDefaults.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDefaults.scala @@ -1,11 +1,11 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume import com.scalableminds.webknossos.datastore.models.datasource.ElementClass -import com.scalableminds.webknossos.datastore.geometry.Vector3D +import com.scalableminds.webknossos.datastore.geometry.Vec3DoubleProto object VolumeTracingDefaults { - val editRotation: Vector3D = Vector3D(0, 0, 0) + val editRotation: Vec3DoubleProto = Vec3DoubleProto(0, 0, 0) val elementClass: ElementClass.Value = ElementClass.uint32 diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDownsampling.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDownsampling.scala index 75065a311ce..5587fedfb03 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDownsampling.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingDownsampling.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume -import com.scalableminds.util.geometry.Point3D +import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.models.{BucketPosition, UnsignedIntegerArray} import com.scalableminds.webknossos.datastore.models.datasource.{DataLayerLike, DataSourceLike, ElementClass} @@ -11,7 +11,7 @@ import com.scalableminds.webknossos.tracingstore.tracings.{ TracingDataStore, VersionedKeyValuePair } -import com.scalableminds.webknossos.datastore.geometry.{Point3D => ProtoPoint3D} +import com.scalableminds.webknossos.datastore.geometry.{Vec3IntProto => ProtoPoint3D} import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits import play.api.libs.json.{Format, Json} @@ -21,13 +21,13 @@ import scala.reflect.ClassTag object VolumeTracingDownsampling { def resolutionsForVolumeTracingByLayerName(dataSource: DataSourceLike, - fallbackLayerName: Option[String]): List[Point3D] = { + fallbackLayerName: Option[String]): List[Vec3Int] = { val fallbackLayer: Option[DataLayerLike] = fallbackLayerName.flatMap(name => dataSource.dataLayers.find(_.name == name)) resolutionsForVolumeTracing(dataSource, fallbackLayer) } - def resolutionsForVolumeTracing(dataSource: DataSourceLike, fallbackLayer: Option[DataLayerLike]): List[Point3D] = { + def resolutionsForVolumeTracing(dataSource: DataSourceLike, fallbackLayer: Option[DataLayerLike]): List[Vec3Int] = { val fallBackLayerMags = fallbackLayer.map(_.resolutions) fallBackLayerMags.getOrElse(dataSource.dataLayers.flatMap(_.resolutions).distinct).sortBy(_.maxDim) } @@ -49,7 +49,7 @@ trait VolumeTracingDownsampling toCache: Boolean = false): Fox[Unit] def downsampleWithLayer(tracingId: String, tracing: VolumeTracing, dataLayer: VolumeTracingLayer)( - implicit ec: ExecutionContext): Fox[List[Point3D]] = { + implicit ec: ExecutionContext): Fox[List[Vec3Int]] = { val bucketVolume = 32 * 32 * 32 for { _ <- bool2Fox(tracing.version == 0L) ?~> "Tracing has already been edited." @@ -84,7 +84,7 @@ trait VolumeTracingDownsampling private def fillMapWithSourceBucketsInplace(bucketDataMap: mutable.HashMap[BucketPosition, Array[Byte]], tracingId: String, dataLayer: VolumeTracingLayer, - sourceMag: Point3D): Unit = { + sourceMag: Vec3Int): Unit = { val data: List[VersionedKeyValuePair[Array[Byte]]] = tracingDataStore.volumeData.getMultipleKeys(tracingId, Some(tracingId)) data.foreach { keyValuePair: VersionedKeyValuePair[Array[Byte]] => @@ -99,8 +99,8 @@ trait VolumeTracingDownsampling } } - private def downsampleMagFromMag(previousMag: Point3D, - requiredMag: Point3D, + private def downsampleMagFromMag(previousMag: Vec3Int, + requiredMag: Vec3Int, originalBucketPositions: List[BucketPosition], bucketDataMapMutable: mutable.HashMap[BucketPosition, Array[Byte]], updatedBucketsMutable: mutable.ListBuffer[BucketPosition], @@ -108,7 +108,7 @@ trait VolumeTracingDownsampling elementClass: ElementClass.Value, dataLayer: VolumeTracingLayer): Unit = { val downScaleFactor = - Point3D(requiredMag.x / previousMag.x, requiredMag.y / previousMag.y, requiredMag.z / previousMag.z) + Vec3Int(requiredMag.x / previousMag.x, requiredMag.y / previousMag.y, requiredMag.z / previousMag.z) downsampledBucketPositions(originalBucketPositions, requiredMag).foreach { downsampledBucketPosition => val sourceBuckets: Seq[BucketPosition] = sourceBucketPositionsFor(downsampledBucketPosition, downScaleFactor, previousMag) @@ -129,7 +129,7 @@ trait VolumeTracingDownsampling } private def downsampledBucketPositions(originalBucketPositions: List[BucketPosition], - requiredMag: Point3D): Set[BucketPosition] = + requiredMag: Vec3Int): Set[BucketPosition] = originalBucketPositions.map { bucketPosition: BucketPosition => BucketPosition( (bucketPosition.globalX / requiredMag.x / 32) * requiredMag.x * 32, @@ -140,8 +140,8 @@ trait VolumeTracingDownsampling }.toSet private def sourceBucketPositionsFor(bucketPosition: BucketPosition, - downScaleFactor: Point3D, - previousMag: Point3D): Seq[BucketPosition] = + downScaleFactor: Vec3Int, + previousMag: Vec3Int): Seq[BucketPosition] = for { z <- 0 until downScaleFactor.z y <- 0 until downScaleFactor.y @@ -167,7 +167,7 @@ trait VolumeTracingDownsampling } private def downsampleData[T: ClassTag](data: Array[Array[T]], - downScaleFactor: Point3D, + downScaleFactor: Vec3Int, bucketVolume: Int): Array[T] = { val result = new Array[T](bucketVolume) for { @@ -181,11 +181,11 @@ trait VolumeTracingDownsampling x_offset <- 0 until downScaleFactor.x } yield { val sourceVoxelPosition = - Point3D(x * downScaleFactor.x + x_offset, y * downScaleFactor.y + y_offset, z * downScaleFactor.z + z_offset) + Vec3Int(x * downScaleFactor.x + x_offset, y * downScaleFactor.y + y_offset, z * downScaleFactor.z + z_offset) val sourceBucketPosition = - Point3D(sourceVoxelPosition.x / 32, sourceVoxelPosition.y / 32, sourceVoxelPosition.z / 32) + Vec3Int(sourceVoxelPosition.x / 32, sourceVoxelPosition.y / 32, sourceVoxelPosition.z / 32) val sourceVoxelPositionInSourceBucket = - Point3D(sourceVoxelPosition.x % 32, sourceVoxelPosition.y % 32, sourceVoxelPosition.z % 32) + Vec3Int(sourceVoxelPosition.x % 32, sourceVoxelPosition.y % 32, sourceVoxelPosition.z % 32) val sourceBucketIndex = sourceBucketPosition.x + sourceBucketPosition.y * downScaleFactor.y + sourceBucketPosition.z * downScaleFactor.y * downScaleFactor.z val sourceVoxelIndex = sourceVoxelPositionInSourceBucket.x + sourceVoxelPositionInSourceBucket.y * 32 + sourceVoxelPositionInSourceBucket.z * 32 * 32 data(sourceBucketIndex)(sourceVoxelIndex) @@ -198,17 +198,17 @@ trait VolumeTracingDownsampling private def mode[T](items: Seq[T]): T = items.groupBy(i => i).mapValues(_.size).maxBy(_._2)._1 - private def getSourceMag(tracing: VolumeTracing): Point3D = + private def getSourceMag(tracing: VolumeTracing): Vec3Int = tracing.resolutions.minBy(_.maxDim) - private def getMagsToCreate(tracing: VolumeTracing): Fox[List[Point3D]] = + private def getMagsToCreate(tracing: VolumeTracing): Fox[List[Vec3Int]] = for { requiredMags <- getRequiredMags(tracing) sourceMag = getSourceMag(tracing) magsToCreate = requiredMags.filter(_.maxDim > sourceMag.maxDim) } yield magsToCreate - protected def getRequiredMags(tracing: VolumeTracing): Fox[List[Point3D]] = + protected def getRequiredMags(tracing: VolumeTracing): Fox[List[Vec3Int]] = for { dataSource: DataSourceLike <- tracingStoreWkRpcClient.getDataSource(tracing.organizationName, tracing.dataSetName) magsForTracing = VolumeTracingDownsampling.resolutionsForVolumeTracingByLayerName(dataSource, @@ -219,8 +219,8 @@ trait VolumeTracingDownsampling resolutionRestrictions: ResolutionRestrictions): VolumeTracing = { val tracingResolutions = resolveLegacyResolutionList(tracing.resolutions) - val allowedResolutions = resolutionRestrictions.filterAllowed(tracingResolutions.map(point3DFromProto)) - tracing.withResolutions(allowedResolutions.map(point3DToProto)) + val allowedResolutions = resolutionRestrictions.filterAllowed(tracingResolutions.map(vec3IntFromProto)) + tracing.withResolutions(allowedResolutions.map(vec3IntToProto)) } protected def resolveLegacyResolutionList(resolutions: Seq[ProtoPoint3D]): Seq[ProtoPoint3D] = @@ -236,13 +236,13 @@ case class ResolutionRestrictions( min: Option[Int], max: Option[Int] ) { - def filterAllowed(resolutions: Seq[Point3D]): Seq[Point3D] = + def filterAllowed(resolutions: Seq[Vec3Int]): Seq[Vec3Int] = resolutions.filter(isAllowed) - def isAllowed(resolution: Point3D): Boolean = + def isAllowed(resolution: Vec3Int): Boolean = min.getOrElse(0) <= resolution.maxDim && max.getOrElse(Int.MaxValue) >= resolution.maxDim - def isForbidden(resolution: Point3D): Boolean = !isAllowed(resolution) + def isForbidden(resolution: Vec3Int): Boolean = !isAllowed(resolution) def minStr: Option[String] = min.map(_.toString) def maxStr: Option[String] = max.map(_.toString) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala index c2eb72c169a..402ff0bd822 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala @@ -1,6 +1,6 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume -import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.dataformats.BucketProvider import com.scalableminds.webknossos.datastore.models.BucketPosition @@ -72,13 +72,13 @@ case class VolumeTracingLayer( isTemporaryTracing: Boolean = false, defaultViewConfiguration: Option[LayerViewConfiguration] = None, adminViewConfiguration: Option[LayerViewConfiguration] = None, - volumeResolutions: List[Point3D] = List.empty + volumeResolutions: List[Vec3Int] = List.empty )(implicit val volumeDataStore: FossilDBClient, implicit val volumeDataCache: TemporaryVolumeDataStore, implicit val temporaryTracingStore: TemporaryTracingStore[VolumeTracing]) extends SegmentationLayer { - def lengthOfUnderlyingCubes(resolution: Point3D): Int = DataLayer.bucketLength + def lengthOfUnderlyingCubes(resolution: Vec3Int): Int = DataLayer.bucketLength val dataFormat: DataFormat.Value = DataFormat.tracing @@ -92,8 +92,8 @@ case class VolumeTracingLayer( val mappings: Option[Set[String]] = None - val resolutions: List[Point3D] = if (volumeResolutions.nonEmpty) volumeResolutions else List(Point3D(1, 1, 1)) + val resolutions: List[Vec3Int] = if (volumeResolutions.nonEmpty) volumeResolutions else List(Vec3Int(1, 1, 1)) - override def containsResolution(resolution: Point3D) = + override def containsResolution(resolution: Vec3Int) = true // allow requesting buckets of all resolutions. database takes care of missing. } diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala index c34b7bea710..987d6d7dba7 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala @@ -1,13 +1,13 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume import com.google.inject.Inject -import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import com.scalableminds.util.io.{NamedStream, ZipIO} import com.scalableminds.util.tools.{Fox, FoxImplicits, TextUtils} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing.ElementClass import com.scalableminds.webknossos.datastore.dataformats.wkw.{WKWBucketStreamSink, WKWDataFormatHelper} -import com.scalableminds.webknossos.datastore.geometry.NamedBoundingBox +import com.scalableminds.webknossos.datastore.geometry.NamedBoundingBoxProto import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits import com.scalableminds.webknossos.datastore.models.DataRequestCollection.DataRequestCollection import com.scalableminds.webknossos.datastore.models.datasource.DataSourceLike @@ -23,9 +23,10 @@ import play.api.libs.Files import play.api.libs.Files.TemporaryFileCreator import play.api.libs.iteratee.Enumerator import play.api.libs.json.{JsObject, JsValue, Json} - import java.io._ import java.nio.file.Paths +import java.util.zip.Deflater + import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -124,15 +125,15 @@ class VolumeTracingService @Inject()( _ <- saveBucket(volumeTracingLayer(tracingId, volumeTracing), bucket, action.data, updateGroupVersion) } yield volumeTracing - private def lookUpVolumeResolution(tracing: VolumeTracing, zoomStep: Int): Fox[Point3D] = + private def lookUpVolumeResolution(tracing: VolumeTracing, zoomStep: Int): Fox[Vec3Int] = if (tracing.resolutions.nonEmpty) { tracing.resolutions .find(r => r.maxDim == math.pow(2, zoomStep)) - .map(point3DFromProto) + .map(vec3IntFromProto) .toFox ?~> s"Received bucket with zoomStep ($zoomStep), no matching resolution found in tracing (has ${tracing.resolutions})" } else { val isotropicResolution = math.pow(2, zoomStep).toInt - Fox.successful(Point3D(isotropicResolution, isotropicResolution, isotropicResolution)) + Fox.successful(Vec3Int(isotropicResolution, isotropicResolution, isotropicResolution)) } private def revertToVolumeVersion(tracingId: String, @@ -155,10 +156,10 @@ class VolumeTracingService @Inject()( sourceTracing } - def initializeWithDataMultiple(tracingId: String, tracing: VolumeTracing, initialData: File): Fox[Set[Point3D]] = { + def initializeWithDataMultiple(tracingId: String, tracing: VolumeTracing, initialData: File): Fox[Set[Vec3Int]] = { if (tracing.version != 0L) return Failure("Tracing has already been edited.") - val resolutionSets = new mutable.HashSet[Set[Point3D]]() + val resolutionSets = new mutable.HashSet[Set[Vec3Int]]() withZipsFromMultiZip(initialData) { (_, dataZip) => val resolutionSet = resolutionSetFromZipfile(dataZip) if (resolutionSet.nonEmpty) resolutionSets.add(resolutionSet) @@ -185,13 +186,13 @@ class VolumeTracingService @Inject()( def initializeWithData(tracingId: String, tracing: VolumeTracing, initialData: File, - resolutionRestrictions: ResolutionRestrictions): Box[Set[Point3D]] = { + resolutionRestrictions: ResolutionRestrictions): Box[Set[Vec3Int]] = { if (tracing.version != 0L) { return Failure("Tracing has already been edited.") } val dataLayer = volumeTracingLayer(tracingId, tracing) - val savedResolutions = new mutable.HashSet[Point3D]() + val savedResolutions = new mutable.HashSet[Vec3Int]() val unzipResult = withBucketsFromZip(initialData) { (bucketPosition, bytes) => if (resolutionRestrictions.isForbidden(bucketPosition.resolution)) { @@ -220,13 +221,16 @@ class VolumeTracingService @Inject()( val buckets: Iterator[NamedStream] = new WKWBucketStreamSink(dataLayer)(dataLayer.bucketProvider.bucketStream(Some(tracing.version))) - val zipResult = ZipIO.zip(buckets, os) + val before = System.currentTimeMillis() + val zipResult = ZipIO.zip(buckets, os, level = Deflater.BEST_SPEED) zipResult.onComplete { case failure: scala.util.Failure[Unit] => logger.debug( s"Failed to send zipped volume data for $tracingId: ${TextUtils.stackTraceAsString(failure.exception)}") - case _: scala.util.Success[Unit] => logger.debug(s"Successfully sent zipped volume data for $tracingId") + case _: scala.util.Success[Unit] => + val after = System.currentTimeMillis() + logger.info(s"Zipping volume data for $tracingId took ${after - before} ms") } zipResult } @@ -250,7 +254,7 @@ class VolumeTracingService @Inject()( largestSegmentId = 0L, fallbackLayer = None, version = 0L, - resolutions = VolumeTracingDownsampling.resolutionsForVolumeTracing(dataSource, None).map(point3DToProto) + resolutions = VolumeTracingDownsampling.resolutionsForVolumeTracing(dataSource, None).map(vec3IntToProto) ) def duplicate(tracingId: String, @@ -276,7 +280,11 @@ class VolumeTracingService @Inject()( val newId = if (tracing.userBoundingBoxes.isEmpty) 1 else tracing.userBoundingBoxes.map(_.id).max + 1 tracing .addUserBoundingBoxes( - NamedBoundingBox(newId, Some("task bounding box"), Some(true), Some(getRandomColor), tracing.boundingBox)) + NamedBoundingBoxProto(newId, + Some("task bounding box"), + Some(true), + Some(getRandomColor), + tracing.boundingBox)) .withBoundingBox(dataSetBoundingBox.get) } else tracing @@ -291,7 +299,7 @@ class VolumeTracingService @Inject()( destinationDataLayer = volumeTracingLayer(destinationId, destinationTracing) _ <- Fox.combined(buckets.map { case (bucketPosition, bucketData) => - if (destinationTracing.resolutions.contains(point3DToProto(bucketPosition.resolution))) { + if (destinationTracing.resolutions.contains(vec3IntToProto(bucketPosition.resolution))) { saveBucket(destinationDataLayer, bucketPosition, bucketData, destinationTracing.version) } else Fox.successful(()) }.toList) @@ -306,7 +314,7 @@ class VolumeTracingService @Inject()( tracing.elementClass, tracing.largestSegmentId, isTemporaryTracing, - volumeResolutions = tracing.resolutions.map(point3DFromProto).toList + volumeResolutions = tracing.resolutions.map(vec3IntFromProto).toList ) def updateActionLog(tracingId: String): Fox[JsValue] = { @@ -325,12 +333,12 @@ class VolumeTracingService @Inject()( def updateResolutionList(tracingId: String, tracing: VolumeTracing, - resolutions: Set[Point3D], + resolutions: Set[Vec3Int], toCache: Boolean = false): Fox[String] = for { _ <- bool2Fox(tracing.version == 0L) ?~> "Tracing has already been edited." _ <- bool2Fox(resolutions.nonEmpty) ?~> "Resolution restrictions result in zero resolutions" - id <- save(tracing.copy(resolutions = resolutions.toList.sortBy(_.maxDim).map(point3DToProto)), + id <- save(tracing.copy(resolutions = resolutions.toList.sortBy(_.maxDim).map(vec3IntToProto)), Some(tracingId), tracing.version, toCache) @@ -351,7 +359,7 @@ class VolumeTracingService @Inject()( segmentationLayer, request.cuboid(segmentationLayer), request.segmentId, - request.voxelDimensions, + request.subsamplingStrides, request.scale, request.mapping, request.mappingType @@ -359,7 +367,7 @@ class VolumeTracingService @Inject()( result <- isosurfaceService.requestIsosurfaceViaActor(isosurfaceRequest) } yield result - def findData(tracingId: String): Fox[Option[Point3D]] = + def findData(tracingId: String): Fox[Option[Vec3Int]] = for { tracing <- find(tracingId) ?~> "tracing.notFound" volumeLayer = volumeTracingLayer(tracingId, tracing) @@ -368,7 +376,7 @@ class VolumeTracingService @Inject()( val bucket = bucketStream.next() val bucketPos = bucket._1 getPositionOfNonZeroData(bucket._2, - Point3D(bucketPos.globalX, bucketPos.globalY, bucketPos.globalZ), + Vec3Int(bucketPos.globalX, bucketPos.globalY, bucketPos.globalZ), volumeLayer.bytesPerElement) } else None } yield bucketPosOpt @@ -392,8 +400,8 @@ class VolumeTracingService @Inject()( tracingA.copy( largestSegmentId = largestSegmentId, boundingBox = mergedBoundingBox.getOrElse( - com.scalableminds.webknossos.datastore.geometry.BoundingBox( - com.scalableminds.webknossos.datastore.geometry.Point3D(0, 0, 0), + com.scalableminds.webknossos.datastore.geometry.BoundingBoxProto( + com.scalableminds.webknossos.datastore.geometry.Vec3IntProto(0, 0, 0), 0, 0, 0)), // should never be empty for volumes @@ -414,10 +422,10 @@ class VolumeTracingService @Inject()( toCache: Boolean): Fox[Unit] = { val elementClass = tracings.headOption.map(_.elementClass).getOrElse(ElementClass.uint8) - val resolutionSets = new mutable.HashSet[Set[Point3D]]() + val resolutionSets = new mutable.HashSet[Set[Vec3Int]]() tracingSelectors.zip(tracings).foreach { case (selector, tracing) => - val resolutionSet = new mutable.HashSet[Point3D]() + val resolutionSet = new mutable.HashSet[Vec3Int]() bucketStreamFromSelector(selector, tracing).foreach { case (bucketPosition, _) => resolutionSet.add(bucketPosition.resolution) @@ -430,7 +438,7 @@ class VolumeTracingService @Inject()( // If none of the tracings contained any volume data. Do not save buckets, do not touch resolution list if (resolutionSets.isEmpty) return Fox.successful(()) - val resolutionsIntersection: Set[Point3D] = resolutionSets.headOption.map { head => + val resolutionsIntersection: Set[Vec3Int] = resolutionSets.headOption.map { head => resolutionSets.foldLeft(head) { (acc, element) => acc.intersect(element) } @@ -464,7 +472,7 @@ class VolumeTracingService @Inject()( val resolutionSet = resolutionSetFromZipfile(zipFile) val resolutionsDoMatch = resolutionSet.isEmpty || resolutionSet == resolveLegacyResolutionList(tracing.resolutions) - .map(point3DFromProto) + .map(vec3IntFromProto) .toSet if (!resolutionsDoMatch) return Fox.failure("annotation.volume.resolutionsDoNotMatch") diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala index 988820e36ee..e20ccd510d3 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeUpdateActions.scala @@ -2,7 +2,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume import java.util.Base64 -import com.scalableminds.util.geometry.{Point3D, Vector3D} +import com.scalableminds.util.geometry.{Vec3Int, Vec3Double} import com.scalableminds.webknossos.datastore.VolumeTracing.{Segment, VolumeTracing} import com.scalableminds.webknossos.datastore.geometry import com.scalableminds.webknossos.datastore.helpers.ProtoGeometryImplicits @@ -22,7 +22,7 @@ trait VolumeUpdateActionHelper { trait ApplyableVolumeAction extends VolumeUpdateAction -case class UpdateBucketVolumeAction(position: Point3D, +case class UpdateBucketVolumeAction(position: Vec3Int, cubeSize: Int, zoomStep: Int, base64Data: String, @@ -43,8 +43,8 @@ object UpdateBucketVolumeAction { case class UpdateTracingVolumeAction( activeSegmentId: Long, - editPosition: Point3D, - editRotation: Vector3D, + editPosition: Vec3Int, + editRotation: Vec3Double, largestSegmentId: Long, zoomLevel: Double, actionTimestamp: Option[Long] = None, @@ -109,7 +109,7 @@ case class UpdateUserBoundingBoxVisibility(boundingBoxId: Option[Int], override def applyOn(tracing: VolumeTracing): VolumeTracing = { - def updateUserBoundingBoxes(): Seq[geometry.NamedBoundingBox] = + def updateUserBoundingBoxes(): Seq[geometry.NamedBoundingBoxProto] = tracing.userBoundingBoxes.map { boundingBox => if (boundingBoxId.forall(_ == boundingBox.id)) boundingBox.copy(isVisible = Some(isVisible)) @@ -172,7 +172,7 @@ object UpdateTdCamera { } case class CreateSegmentVolumeAction(id: Long, - anchorPosition: Option[Point3D], + anchorPosition: Option[Vec3Int], name: Option[String], creationTime: Option[Long], actionTimestamp: Option[Long] = None) @@ -186,7 +186,7 @@ case class CreateSegmentVolumeAction(id: Long, CompactVolumeUpdateAction("createSegment", actionTimestamp, Json.obj("id" -> id)) override def applyOn(tracing: VolumeTracing): VolumeTracing = { - val newSegment = Segment(id, anchorPosition.map(point3DToProto), name, creationTime) + val newSegment = Segment(id, anchorPosition.map(vec3IntToProto), name, creationTime) tracing.addSegments(newSegment) } } @@ -196,7 +196,7 @@ object CreateSegmentVolumeAction { } case class UpdateSegmentVolumeAction(id: Long, - anchorPosition: Option[Point3D], + anchorPosition: Option[Vec3Int], name: Option[String], creationTime: Option[Long], actionTimestamp: Option[Long] = None) @@ -213,7 +213,7 @@ case class UpdateSegmentVolumeAction(id: Long, override def applyOn(tracing: VolumeTracing): VolumeTracing = { def segmentTransform(segment: Segment): Segment = segment.copy( - anchorPosition = anchorPosition.map(point3DToProto), + anchorPosition = anchorPosition.map(vec3IntToProto), name = name, creationTime = creationTime ) diff --git a/yarn.lock b/yarn.lock index bb0ed541654..a64555b8287 100644 --- a/yarn.lock +++ b/yarn.lock @@ -80,6 +80,13 @@ "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents" chokidar "^3.4.0" +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -1192,11 +1199,40 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" integrity sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA== +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@fortawesome/fontawesome-free@^5.14.0": version "5.15.1" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz#ccfef6ddbe59f8fe8f694783e1d3eb88902dc5eb" integrity sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ== +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + "@hutson/parse-repository-url@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" @@ -1733,6 +1769,13 @@ dependencies: "@types/node" "*" +"@types/yauzl@^2.9.1": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" + integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== + dependencies: + "@types/node" "*" + "@use-it/interval@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@use-it/interval/-/interval-1.0.0.tgz#c42c68f22ca29a0dc929041746373d94496d2b3a" @@ -2006,6 +2049,11 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.2.0: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + acorn-node@^1.6.1: version "1.8.2" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" @@ -2045,7 +2093,7 @@ acorn@^6.0.1, acorn@^6.0.7: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^7.0.0, acorn@^7.1.1: +acorn@^7.0.0, acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -2072,12 +2120,12 @@ add-stream@^1.0.0: resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= -agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: - es6-promisify "^5.0.0" + debug "4" aggregate-error@^3.0.0: version "3.1.0" @@ -2131,7 +2179,7 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.0: +ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0: version "8.9.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.9.0.tgz#738019146638824dea25edcf299dcba1b0e7eb18" integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ== @@ -2148,6 +2196,11 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -2190,6 +2243,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-regex@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" @@ -2988,7 +3046,7 @@ buffer-writer@2.0.0: resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== -buffer@^5.5.0: +buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -3040,9 +3098,9 @@ cacheable-request@^6.0.0: responselike "^1.0.2" cached-path-relative@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" - integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.1.0.tgz#865576dfef39c0d6a7defde794d078f5308e3ef3" + integrity sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA== caching-transform@^3.0.1: version "3.0.2" @@ -3599,7 +3657,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.6.0, concat-stream@^1.6.2: +concat-stream@^1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -3942,6 +4000,13 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +core_d@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/core_d/-/core_d-3.2.0.tgz#c4cbed5b69684d04a6b5f880935be1659601151c" + integrity sha512-waKkgHU2P19huhuMjCqCDWTYjxCIHoB+nnYjI7pVMUOC1giWxMNDrXkPw9QjWY+PWCFm49bD3wA/J+c7BGZ+og== + dependencies: + supports-color "^8.1.0" + cosmiconfig@^5.0.2: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -4013,7 +4078,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -4211,6 +4276,20 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@4, debug@^4.0.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +debug@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -4218,13 +4297,6 @@ debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.0: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -4298,6 +4370,11 @@ deep-freeze@0.0.1: resolved "https://registry.yarnpkg.com/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84" integrity sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ= +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -4431,6 +4508,11 @@ detective@^5.2.0: defined "^1.0.0" minimist "^1.1.1" +devtools-protocol@0.0.901419: + version "0.0.901419" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd" + integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ== + dice-coefficient@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/dice-coefficient/-/dice-coefficient-1.0.5.tgz#f99be235d27fe886fbadac107fd6fd874f6b7b5d" @@ -4809,6 +4891,13 @@ enhanced-resolve@^5.8.3: graceful-fs "^4.2.4" tapable "^2.2.0" +enquirer@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + entities@^1.1.1, entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -4990,18 +5079,6 @@ es6-promise@^3.0.2: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -5190,7 +5267,7 @@ eslint-scope@3.7.1: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@5.1.1, eslint-scope@^5.0.0: +eslint-scope@5.1.1, eslint-scope@^5.0.0, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -5213,11 +5290,23 @@ eslint-utils@^1.3.1, eslint-utils@^1.4.3: dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + eslint@^5.7.0: version "5.16.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" @@ -5303,6 +5392,62 @@ eslint@^6.0.1: text-table "^0.2.0" v8-compile-cache "^2.0.3" +eslint@^7.3.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== + dependencies: + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.1.2" + globals "^13.6.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.9" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +eslint_d@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/eslint_d/-/eslint_d-11.1.1.tgz#206ef2de9f699ab642f5bc3fee6f80d43a66f0f0" + integrity sha512-PaNWblwIa10KZUt9EObGBzrsaxB+CPtk5d99sTveXmAtQrPSmrntKpWqxN/Mwot0qnI5gCllzkPZi6tO710KUA== + dependencies: + core_d "^3.2.0" + eslint "^7.3.0" + nanolru "^1.0.0" + optionator "^0.9.1" + espree@^3.5.4: version "3.5.4" resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" @@ -5329,6 +5474,15 @@ espree@^6.1.2: acorn-jsx "^5.2.0" eslint-visitor-keys "^1.1.0" +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -5341,6 +5495,13 @@ esquery@^1.0.1: dependencies: estraverse "^5.1.0" +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.1.0, esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -5538,15 +5699,16 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@^1.6.6: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" + debug "^4.1.1" + get-stream "^5.1.0" yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" extsprintf@1.3.0: version "1.3.0" @@ -5601,7 +5763,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -5688,6 +5850,13 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + file-saver@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" @@ -5826,11 +5995,24 @@ flat-cache@^2.0.1: rimraf "2.6.3" write "1.0.3" +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + flatted@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatted@^3.1.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== + flexlayout-react@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/flexlayout-react/-/flexlayout-react-0.5.5.tgz#e96dbb61bd29868836aff5139d558fb13abf1287" @@ -5881,9 +6063,9 @@ flush-write-stream@^1.0.2: readable-stream "^2.3.6" follow-redirects@^1.0.0: - version "1.14.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" - integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== for-in@^1.0.2: version "1.0.2" @@ -6338,6 +6520,13 @@ globals@^12.1.0: dependencies: type-fest "^0.8.1" +globals@^13.6.0, globals@^13.9.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.0.tgz#4d733760304230a0082ed96e21e5c565f898089e" + integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== + dependencies: + type-fest "^0.20.2" + globby@^11.0.1: version "11.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" @@ -6751,13 +6940,13 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-proxy-agent@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== +https-proxy-agent@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== dependencies: - agent-base "^4.3.0" - debug "^3.1.0" + agent-base "6" + debug "4" human-signals@^2.1.0: version "2.1.0" @@ -6867,6 +7056,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-js@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/import-js/-/import-js-3.2.0.tgz#0cbde91925258648508266bb0bc5e4939115fbdc" @@ -7990,6 +8187,14 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" @@ -8251,6 +8456,11 @@ lodash.isplainobject@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -8276,6 +8486,11 @@ lodash.throttle@^4.0.1, lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + lodash.upperfirst@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" @@ -8821,7 +9036,7 @@ mime@1.6.0, mime@^1.3.4, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.0.3, mime@^2.2.0: +mime@^2.2.0: version "2.4.6" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== @@ -8947,7 +9162,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -9066,6 +9281,11 @@ nanoid@^3.1.30: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== +nanolru@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nanolru/-/nanolru-1.0.0.tgz#0a5679cd4e4578c4ca3741e610b71c4c9b5afaf8" + integrity sha512-GyQkE8M32pULhQk7Sko5raoIbPalAk90ICG+An4fq6fCsFHsP6fB2K46WGXVdoJpy4SGMnZ/EKbo123fZJomWg== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -9159,6 +9379,13 @@ node-fetch@2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" + integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -9583,6 +9810,18 @@ optionator@^0.8.1, optionator@^0.8.2, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + ora@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ora/-/ora-5.1.0.tgz#b188cf8cd2d4d9b13fd25383bc3e5cba352c94f8" @@ -10124,6 +10363,13 @@ pkg-conf@^3.1.0: find-up "^3.0.0" load-json-file "^5.2.0" +pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -10138,13 +10384,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0, pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - please-upgrade-node@^3.0.2: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -10276,6 +10515,11 @@ prebuild-install@^5.1.0: tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -10331,7 +10575,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.0, progress@^2.0.1: +progress@2.0.3, progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -10413,7 +10657,7 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" -proxy-from-env@^1.0.0: +proxy-from-env@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -10475,19 +10719,23 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" -puppeteer@^1.13.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.20.0.tgz#e3d267786f74e1d87cf2d15acc59177f471bbe38" - integrity sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ== - dependencies: - debug "^4.1.0" - extract-zip "^1.6.6" - https-proxy-agent "^2.2.1" - mime "^2.0.3" - progress "^2.0.1" - proxy-from-env "^1.0.0" - rimraf "^2.6.1" - ws "^6.1.0" +puppeteer@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-11.0.0.tgz#0808719c38e15315ecc1b1c28911f1c9054d201f" + integrity sha512-6rPFqN1ABjn4shgOICGDBITTRV09EjXVqhDERBDKwCLz0UyBxeeBH6Ay0vQUJ84VACmlxwzOIzVEJXThcF3aNg== + dependencies: + debug "4.3.2" + devtools-protocol "0.0.901419" + extract-zip "2.0.1" + https-proxy-agent "5.0.0" + node-fetch "2.6.5" + pkg-dir "4.2.0" + progress "2.0.3" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.2.3" q@^1.1.2, q@^1.5.1: version "1.5.1" @@ -11433,6 +11681,11 @@ regexpp@^2.0.1: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== +regexpp@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + regexpu-core@^4.7.1: version "4.7.1" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" @@ -11804,6 +12057,13 @@ rimraf@2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" +rimraf@3.0.2, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -11811,13 +12071,6 @@ rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" @@ -11975,7 +12228,7 @@ semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.1.1, semver@^7.3.4, semver@^7.3.5: +semver@^7.1.1, semver@^7.2.1, semver@^7.3.4, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -12156,9 +12409,9 @@ simple-concat@^1.0.0: integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== simple-get@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" - integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + version "3.1.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" + integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== dependencies: decompress-response "^4.2.0" once "^1.3.1" @@ -12214,6 +12467,15 @@ slice-ansi@^3.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -12583,6 +12845,15 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string.prototype.matchall@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz#24243399bc31b0a49d19e2b74171a15653ec996a" @@ -12691,6 +12962,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -12779,7 +13057,7 @@ supports-color@^7.1.0, supports-color@^7.2.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: +supports-color@^8.0.0, supports-color@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -12830,6 +13108,17 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" +table@^6.0.9: + version "6.8.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" + integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + taffydb@2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" @@ -12845,7 +13134,7 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@^2.0.0: +tar-fs@2.1.1, tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -12988,7 +13277,7 @@ through2@^4.0.0: dependencies: readable-stream "3" -through@2, "through@>=2.2.7 <3", through@^2.3.6: +through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -13176,6 +13465,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -13198,6 +13494,11 @@ type-fest@^0.18.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -13267,6 +13568,14 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.5.tgz#cdabb7d4954231d80cb4a927654c4655e51f4859" integrity sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ== +unbzip2-stream@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -13906,7 +14215,7 @@ winston@^2.3.1: isstream "0.1.x" stack-trace "0.0.x" -word-wrap@~1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -13989,6 +14298,11 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +ws@8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + ws@^5.2.0: version "5.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" @@ -13996,13 +14310,6 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" -ws@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - ws@^8.1.0: version "8.4.2" resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b"