diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 3917837e33f..7573158053d 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Added - Added a rudimentary version of openAPI docs for some routes. Available at `/swagger.json`. [#5693](https://github.com/scalableminds/webknossos/pull/5693) +- Added support for datasets that have multiple segmentation layers. Note that only one segmentation layer can be rendered at a time, currently. [#5683](https://github.com/scalableminds/webknossos/pull/5683) - Added shortcuts K and L for toggling the left and right sidebars. [#5709](https://github.com/scalableminds/webknossos/pull/5709) ### Changed diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md index 235daea52d4..801176a065c 100644 --- a/MIGRATIONS.unreleased.md +++ b/MIGRATIONS.unreleased.md @@ -7,6 +7,8 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md). ## Unreleased +- For webknossos.org: Change `publicDemoDatasetUrl` in the `features`-block within `application.conf` to be an actionable URL. For example, append `/createExplorative/hybrid?fallbackLayerName=segmentation` to the URL so that a new annotation is created if a user clicks on `Open a Demo Dataset` in the dashboard. + ### Postgres Evolutions: - [075-tasktype-remove-hovered-cell-id.sql](conf/evolutions/075-tasktype-remove-hovered-cell-id.sql) diff --git a/app/controllers/AnnotationController.scala b/app/controllers/AnnotationController.scala index ec59b1bac03..f0e49a3d731 100755 --- a/app/controllers/AnnotationController.scala +++ b/app/controllers/AnnotationController.scala @@ -29,7 +29,7 @@ import scala.concurrent.ExecutionContext import scala.concurrent.duration._ case class CreateExplorationalParameters(typ: String, - withFallback: Option[Boolean], + fallbackLayerName: Option[String], resolutionRestrictions: Option[ResolutionRestrictions]) object CreateExplorationalParameters { implicit val jsonFormat: OFormat[CreateExplorationalParameters] = Json.format[CreateExplorationalParameters] @@ -178,7 +178,7 @@ class AnnotationController @Inject()( request.identity, dataSet._id, tracingType, - request.body.withFallback.getOrElse(true), + request.body.fallbackLayerName, request.body.resolutionRestrictions.getOrElse(ResolutionRestrictions.empty) ) ?~> "annotation.create.failed" _ = analyticsService.track(CreateAnnotationEvent(request.identity: User, annotation: Annotation)) @@ -188,16 +188,17 @@ class AnnotationController @Inject()( } @ApiOperation(hidden = true, value = "") - def makeHybrid(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => - for { - _ <- bool2Fox(AnnotationType.Explorational.toString == typ) ?~> "annotation.makeHybrid.explorationalsOnly" - annotation <- provider.provideAnnotation(typ, id, request.identity) - organization <- organizationDAO.findOne(request.identity._organization) - _ <- annotationService.makeAnnotationHybrid(annotation, organization.name) ?~> "annotation.makeHybrid.failed" - updated <- provider.provideAnnotation(typ, id, request.identity) - json <- annotationService.publicWrites(updated, Some(request.identity)) ?~> "annotation.write.failed" - } yield JsonOk(json) - } + def makeHybrid(typ: String, id: String, fallbackLayerName: Option[String]): Action[AnyContent] = + sil.SecuredAction.async { implicit request => + for { + _ <- bool2Fox(AnnotationType.Explorational.toString == typ) ?~> "annotation.makeHybrid.explorationalsOnly" + annotation <- provider.provideAnnotation(typ, id, request.identity) + organization <- organizationDAO.findOne(request.identity._organization) + _ <- annotationService.makeAnnotationHybrid(annotation, organization.name, fallbackLayerName) ?~> "annotation.makeHybrid.failed" + updated <- provider.provideAnnotation(typ, id, request.identity) + json <- annotationService.publicWrites(updated, Some(request.identity)) ?~> "annotation.write.failed" + } yield JsonOk(json) + } @ApiOperation(hidden = true, value = "") def downsample(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => diff --git a/app/controllers/LegacyApiController.scala b/app/controllers/LegacyApiController.scala index d47678de36c..5af34d46b0b 100644 --- a/app/controllers/LegacyApiController.scala +++ b/app/controllers/LegacyApiController.scala @@ -147,7 +147,7 @@ class LegacyApiController @Inject()(annotationController: AnnotationController, def annotationMakeHybrid(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { - result <- annotationController.makeHybrid(typ, id)(request) + result <- annotationController.makeHybrid(typ, id, None)(request) } yield replaceVisibilityInResultJson(result) } diff --git a/app/models/annotation/AnnotationService.scala b/app/models/annotation/AnnotationService.scala index f273cb44be3..d8554f4d2d1 100755 --- a/app/models/annotation/AnnotationService.scala +++ b/app/models/annotation/AnnotationService.scala @@ -139,17 +139,23 @@ class AnnotationService @Inject()( dataSet: DataSet, dataSource: DataSource, tracingType: TracingType.Value, - withFallback: Boolean, + fallbackLayerNameOpt: Option[String], resolutionRestrictions: ResolutionRestrictions, organizationName: String, oldTracingId: Option[String] = None)(implicit ctx: DBAccessContext): Fox[(Option[String], Option[String])] = { - def getFallbackLayer: Option[SegmentationLayer] = - if (withFallback) { - dataSource.dataLayers.flatMap { - case layer: SegmentationLayer => Some(layer) - case _ => None - }.headOption - } else None + + def getFallbackLayer(fallbackLayerName: String): Fox[SegmentationLayer] = + for { + fallbackLayer <- dataSource.dataLayers + .filter(dl => dl.name == fallbackLayerName) + .flatMap { + case layer: SegmentationLayer => Some(layer) + case _ => None + } + .headOption + .toFox + _ <- bool2Fox(fallbackLayer.elementClass != ElementClass.uint64) ?~> "annotation.volume.uint64" + } yield fallbackLayer tracingType match { case TracingType.skeleton => @@ -170,8 +176,7 @@ class AnnotationService @Inject()( case TracingType.volume => for { client <- tracingStoreService.clientFor(dataSet) - fallbackLayer = getFallbackLayer - _ <- bool2Fox(fallbackLayer.forall(_.elementClass != ElementClass.uint64)) ?~> "annotation.volume.uint64" + fallbackLayer <- Fox.runOptional(fallbackLayerNameOpt)(getFallbackLayer) volumeTracing <- createVolumeTracing(dataSource, organizationName, fallbackLayer, @@ -181,8 +186,7 @@ class AnnotationService @Inject()( case TracingType.hybrid => for { client <- tracingStoreService.clientFor(dataSet) - fallbackLayer = getFallbackLayer - _ <- bool2Fox(fallbackLayer.forall(_.elementClass != ElementClass.uint64)) ?~> "annotation.volume.uint64" + fallbackLayer <- Fox.runOptional(fallbackLayerNameOpt)(getFallbackLayer) skeletonTracingId <- client.saveSkeletonTracing( SkeletonTracingDefaults.createInstance.copy(dataSetName = dataSet.name, editPosition = dataSource.center, @@ -199,7 +203,7 @@ class AnnotationService @Inject()( def createExplorationalFor(user: User, _dataSet: ObjectId, tracingType: TracingType.Value, - withFallback: Boolean, + fallbackLayerName: Option[String], resolutionRestrictions: ResolutionRestrictions)(implicit ctx: DBAccessContext, m: MessagesProvider): Fox[Annotation] = for { @@ -210,7 +214,7 @@ class AnnotationService @Inject()( tracingIds <- createTracingsForExplorational(dataSet, usableDataSource, tracingType, - withFallback, + fallbackLayerName, resolutionRestrictions, organization.name) teamId <- selectSuitableTeam(user, dataSet) ?~> "annotation.create.forbidden" @@ -228,27 +232,29 @@ class AnnotationService @Inject()( annotation } - def makeAnnotationHybrid(annotation: Annotation, organizationName: String)( + def makeAnnotationHybrid(annotation: Annotation, organizationName: String, fallbackLayerName: Option[String])( implicit ctx: DBAccessContext): Fox[Unit] = { def createNewTracings(dataSet: DataSet, dataSource: DataSource) = annotation.tracingType match { case TracingType.skeleton => createTracingsForExplorational(dataSet, dataSource, TracingType.volume, - withFallback = true, + fallbackLayerName, ResolutionRestrictions.empty, organizationName).flatMap { case (_, Some(volumeId)) => annotationDAO.updateVolumeTracingId(annotation._id, volumeId) case _ => Fox.failure("unexpectedReturn") } case TracingType.volume => - createTracingsForExplorational(dataSet, - dataSource, - TracingType.skeleton, - withFallback = false, - ResolutionRestrictions.empty, - organizationName, - annotation.volumeTracingId).flatMap { + createTracingsForExplorational( + dataSet, + dataSource, + TracingType.skeleton, + fallbackLayerNameOpt = None, // unused when creating skeleton + ResolutionRestrictions.empty, + organizationName, + annotation.volumeTracingId + ).flatMap { case (Some(skeletonId), _) => annotationDAO.updateSkeletonTracingId(annotation._id, skeletonId) case _ => Fox.failure("unexpectedReturn") } diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index e3d55b8ea82..c11243b7c2e 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -114,7 +114,7 @@ PUT /annotations/:typ/:id/reset c PATCH /annotations/:typ/:id/transfer controllers.AnnotationController.transfer(typ: String, id: String) GET /annotations/:typ/:id/info controllers.AnnotationController.info(typ: String, id: String, timestamp: Long) -PATCH /annotations/:typ/:id/makeHybrid controllers.AnnotationController.makeHybrid(typ: String, id: String) +PATCH /annotations/:typ/:id/makeHybrid controllers.AnnotationController.makeHybrid(typ: String, id: String, fallbackLayerName: Option[String]) PATCH /annotations/:typ/:id/downsample controllers.AnnotationController.downsample(typ: String, id: String) PATCH /annotations/:typ/:id/unlinkFallback controllers.AnnotationController.unlinkFallback(typ: String, id: String) DELETE /annotations/:typ/:id controllers.AnnotationController.cancel(typ: String, id: String) diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index d7148b1e6b8..d5ff1c390cb 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -675,14 +675,14 @@ export function getAnnotationInformation( export function createExplorational( datasetId: APIDatasetId, typ: TracingType, - withFallback: boolean, + fallbackLayerName: ?string, resolutionRestrictions: ?APIResolutionRestrictions, options?: RequestOptions = {}, ): Promise { const url = `/api/datasets/${datasetId.owningOrganization}/${datasetId.name}/createExplorational`; return Request.sendJSONReceiveJSON( url, - Object.assign({}, { data: { typ, withFallback, resolutionRestrictions } }, options), + Object.assign({}, { data: { typ, fallbackLayerName, resolutionRestrictions } }, options), ); } @@ -761,9 +761,13 @@ export async function importVolumeTracing(tracing: Tracing, dataFile: File): Pro ); } -export function convertToHybridTracing(annotationId: string): Promise { +export function convertToHybridTracing( + annotationId: string, + fallbackLayerName: ?string, +): Promise { return Request.receiveJSON(`/api/annotations/Explorational/${annotationId}/makeHybrid`, { method: "PATCH", + fallbackLayerName, }); } diff --git a/frontend/javascripts/dashboard/advanced_dataset/create_explorative_modal.js b/frontend/javascripts/dashboard/advanced_dataset/create_explorative_modal.js index aa9e9cfbd01..f011afb7796 100644 --- a/frontend/javascripts/dashboard/advanced_dataset/create_explorative_modal.js +++ b/frontend/javascripts/dashboard/advanced_dataset/create_explorative_modal.js @@ -1,5 +1,5 @@ // @flow -import { Modal, Radio, Button, Checkbox, Tooltip, Slider, Spin } from "antd"; +import { Modal, Radio, Button, Tooltip, Slider, Spin } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; import React, { useState } from "react"; import type { APIDatasetId } from "types/api_flow_types"; @@ -8,10 +8,10 @@ import { useFetch } from "libs/react_helpers"; import { getDataset } from "admin/admin_rest_api"; import { - getSegmentationLayer, doesSupportVolumeWithFallback, getDatasetResolutionInfo, - getResolutionInfoOfSegmentationLayer, + getSegmentationLayers, + getResolutionInfo, } from "oxalis/model/accessors/dataset_accessor"; type Props = { @@ -22,52 +22,33 @@ type Props = { const CreateExplorativeModal = ({ datasetId, onClose }: Props) => { const dataset = useFetch(() => getDataset(datasetId), null, [datasetId]); const [annotationType, setAnnotationType] = useState("hybrid"); - const [userDefinedWithFallback, setUserDefinedWithFallback] = useState(true); const [userDefinedResolutionIndices, setUserDefinedResolutionIndices] = useState([0, 10000]); + const [selectedSegmentationLayerIndex, setSelectedSegmentationLayerIndex] = useState(null); let modalContent = ; if (dataset !== null) { - const segmentationLayer = getSegmentationLayer(dataset); - - const isFallbackSegmentationAlwaysOff = - segmentationLayer == null || - (!doesSupportVolumeWithFallback(dataset) && annotationType !== "skeleton"); - const isFallbackSegmentationAlwaysOn = - !isFallbackSegmentationAlwaysOff && annotationType === "skeleton"; - - const isFallbackSegmentationOptional = - !isFallbackSegmentationAlwaysOff && !isFallbackSegmentationAlwaysOn; - - const isFallbackSegmentationSelected = - isFallbackSegmentationAlwaysOn || - (userDefinedWithFallback && !isFallbackSegmentationAlwaysOff); - - const isFallbackSegmentationSelectedString = isFallbackSegmentationSelected ? "true" : "false"; - - const fallbackCheckbox = ( - setUserDefinedWithFallback(e.target.checked)} - checked={isFallbackSegmentationSelected} - disabled={!isFallbackSegmentationOptional} - style={{ marginBottom: 16 }} - > - With Existing Segmentation{" "} - - - - - ); + const segmentationLayers = getSegmentationLayers(dataset); + const selectedSegmentationLayer = + annotationType !== "skeleton" && + segmentationLayers.length > 0 && + selectedSegmentationLayerIndex != null + ? segmentationLayers[selectedSegmentationLayerIndex] + : null; + + const fallbackLayerGetParameter = + selectedSegmentationLayer != null + ? `&fallbackLayerName=${selectedSegmentationLayer.name}` + : ""; const datasetResolutionInfo = getDatasetResolutionInfo(dataset); let highestResolutionIndex = datasetResolutionInfo.getHighestResolutionIndex(); let lowestResolutionIndex = datasetResolutionInfo.getClosestExistingIndex(0); - if (isFallbackSegmentationSelected && annotationType !== "skeleton") { - const datasetFallbackLayerResolutionInfo = getResolutionInfoOfSegmentationLayer(dataset); + if (selectedSegmentationLayer != null) { + const datasetFallbackLayerResolutionInfo = getResolutionInfo( + selectedSegmentationLayer.resolutions, + ); highestResolutionIndex = datasetFallbackLayerResolutionInfo.getHighestResolutionIndex(); lowestResolutionIndex = datasetFallbackLayerResolutionInfo.getClosestExistingIndex(0); } @@ -75,46 +56,46 @@ const CreateExplorativeModal = ({ datasetId, onClose }: Props) => { const highResolutionIndex = Math.min(highestResolutionIndex, userDefinedResolutionIndices[1]); const lowResolutionIndex = Math.max(lowestResolutionIndex, userDefinedResolutionIndices[0]); - const resolutionSlider = ( - -
- Volume Resolutions{" "} - +
+ Restrict Volume Resolutions{" "} + + + +
+
- - -
-
-
- {datasetResolutionInfo.getResolutionByIndexOrThrow(lowResolutionIndex).join("-")} +
+ {datasetResolutionInfo.getResolutionByIndexOrThrow(lowResolutionIndex).join("-")} +
+ setUserDefinedResolutionIndices(value)} + range + step={1} + min={lowestResolutionIndex} + max={highestResolutionIndex} + value={[lowResolutionIndex, highResolutionIndex]} + style={{ flexGrow: 1 }} + /> +
+ {datasetResolutionInfo.getResolutionByIndexOrThrow(highResolutionIndex).join("-")} +
- setUserDefinedResolutionIndices(value)} - range - disabled={annotationType === "skeleton"} - step={1} - min={lowestResolutionIndex} - max={highestResolutionIndex} - value={[lowResolutionIndex, highResolutionIndex]} - style={{ flexGrow: 1 }} - /> -
- {datasetResolutionInfo.getResolutionByIndexOrThrow(highResolutionIndex).join("-")} -
-
-
- ); + + ) : null; modalContent = ( @@ -125,17 +106,49 @@ const CreateExplorativeModal = ({ datasetId, onClose }: Props) => { Volume only - {doesSupportVolumeWithFallback(dataset) ? fallbackCheckbox : null} + + {annotationType !== "skeleton" && segmentationLayers.length > 0 ? ( +
+ Base Volume Annotation On{" "} + + + + { + const index = parseInt(e.target.value); + setSelectedSegmentationLayerIndex(index !== -1 ? index : null); + }} + value={selectedSegmentationLayerIndex != null ? selectedSegmentationLayerIndex : -1} + > + + Create empty layer + + {segmentationLayers.map((segmentationLayer, index) => ( + + {segmentationLayer.name} + + ))} + +
+ ) : null} + {lowestResolutionIndex < highestResolutionIndex ? resolutionSlider : null}
)} @@ -628,7 +631,7 @@ class DatasetSettings extends React.PureComponent { if (volumeTracing == null) { return []; } - const segmentationLayer = Model.getSegmentationLayer(); + const segmentationLayer = Model.getEnforcedSegmentationTracingLayer(); const { fallbackLayerInfo } = segmentationLayer; const volumeTargetResolutions = fallbackLayerInfo != null @@ -785,10 +788,9 @@ class DatasetSettings extends React.PureComponent { render() { const { layers } = this.props.datasetConfiguration; - const segmentationLayerName = Model.getSegmentationLayerName(); const layerSettings = Object.entries(layers).map(entry => { const [layerName, layer] = entry; - const isColorLayer = segmentationLayerName !== layerName; + const isColorLayer = getIsColorLayer(this.props.dataset, layerName); // $FlowIssue[incompatible-call] Object.entries returns mixed for Flow return this.getLayerSettings(layerName, layer, isColorLayer); }); diff --git a/frontend/javascripts/oxalis/view/left-border-tabs/mapping_settings_view.js b/frontend/javascripts/oxalis/view/left-border-tabs/mapping_settings_view.js index c05ebebba2c..09616d377da 100644 --- a/frontend/javascripts/oxalis/view/left-border-tabs/mapping_settings_view.js +++ b/frontend/javascripts/oxalis/view/left-border-tabs/mapping_settings_view.js @@ -12,7 +12,10 @@ import { type OrthoView, type Vector3 } from "oxalis/constants"; import { type OxalisState, type Mapping, type MappingType } from "oxalis/store"; import { getMappingsForDatasetLayer, getAgglomeratesForDatasetLayer } from "admin/admin_rest_api"; import { getPosition } from "oxalis/model/accessors/flycam_accessor"; -import { getSegmentationLayer } from "oxalis/model/accessors/dataset_accessor"; +import { + getSegmentationLayerByName, + getMappingInfo, +} from "oxalis/model/accessors/dataset_accessor"; import { getVolumeTracing } from "oxalis/model/accessors/volumetracing_accessor"; import { setLayerMappingsAction } from "oxalis/model/actions/dataset_actions"; import { @@ -23,12 +26,14 @@ import Model from "oxalis/model"; import { SwitchSetting } from "oxalis/view/components/setting_input_views"; import * as Utils from "libs/utils"; import { jsConvertCellIdToHSLA } from "oxalis/shaders/segmentation.glsl"; -import DataLayer from "oxalis/model/data_layer"; import { AsyncButton } from "components/async_clickables"; import { loadAgglomerateSkeletonAtPosition } from "oxalis/controller/combinations/segmentation_handlers"; const { Option, OptGroup } = Select; +type OwnProps = {| + layerName: string, +|}; type StateProps = {| dataset: APIDataset, segmentationLayer: ?APISegmentationLayer, @@ -39,15 +44,15 @@ type StateProps = {| hideUnmappedIds: ?boolean, mappingType: MappingType, mappingColors: ?Array, - setMappingEnabled: boolean => void, - setHideUnmappedIds: boolean => void, + setMappingEnabled: (string, boolean) => void, + setHideUnmappedIds: (string, boolean) => void, setAvailableMappingsForLayer: (string, Array, Array) => void, activeViewport: OrthoView, activeCellId: number, isMergerModeEnabled: boolean, allowUpdate: boolean, |}; -type Props = {| ...StateProps |}; +type Props = {| ...OwnProps, ...StateProps |}; type State = { // shouldMappingBeEnabled is the UI state which is directly connected to the @@ -64,7 +69,7 @@ const convertHSLAToCSSString = ([h, s, l, a]) => `hsla(${360 * h}, ${100 * s}%, export const convertCellIdToCSS = (id: number, customColors: ?Array, alpha?: number) => id === 0 ? "transparent" : convertHSLAToCSSString(jsConvertCellIdToHSLA(id, customColors, alpha)); -const hasSegmentation = () => Model.getSegmentationLayer() != null; +const hasSegmentation = () => Model.getVisibleSegmentationLayer() != null; const needle = "##"; const packMappingNameAndCategory = (mappingName, category) => `${category}${needle}${mappingName}`; @@ -82,16 +87,8 @@ class MappingSettingsView extends React.Component { didRefreshMappingList: false, }; - getSegmentationLayer(): DataLayer { - const layer = Model.getSegmentationLayer(); - if (!layer) { - throw new Error("No segmentation layer found"); - } - return layer; - } - handleChangeHideUnmappedSegments = (hideUnmappedIds: boolean) => { - this.props.setHideUnmappedIds(hideUnmappedIds); + this.props.setHideUnmappedIds(this.props.layerName, hideUnmappedIds); }; handleChangeMapping = (packedMappingNameWithCategory: string): void => { @@ -105,7 +102,11 @@ class MappingSettingsView extends React.Component { pauseDelay: 500, successMessageDelay: 2000, }); - Model.getSegmentationLayer().setActiveMapping(mappingName, mappingType, progressCallback); + Model.getLayerByName(this.props.layerName).setActiveMapping( + mappingName, + mappingType, + progressCallback, + ); if (document.activeElement) document.activeElement.blur(); }; @@ -143,7 +144,7 @@ class MappingSettingsView extends React.Component { } this.setState({ shouldMappingBeEnabled }); if (this.props.mappingName != null) { - this.props.setMappingEnabled(shouldMappingBeEnabled); + this.props.setMappingEnabled(this.props.layerName, shouldMappingBeEnabled); } }; @@ -275,18 +276,22 @@ const mapDispatchToProps = { setHideUnmappedIds: setHideUnmappedIdsAction, }; -function mapStateToProps(state: OxalisState) { +function mapStateToProps(state: OxalisState, ownProps: OwnProps) { + const activeMappingInfo = getMappingInfo( + state.temporaryConfiguration.activeMappingByLayer, + ownProps.layerName, + ); return { dataset: state.dataset, position: getPosition(state.flycam), - hideUnmappedIds: state.temporaryConfiguration.activeMapping.hideUnmappedIds, - isMappingEnabled: state.temporaryConfiguration.activeMapping.isMappingEnabled, - mapping: state.temporaryConfiguration.activeMapping.mapping, - mappingName: state.temporaryConfiguration.activeMapping.mappingName, - mappingType: state.temporaryConfiguration.activeMapping.mappingType, - mappingColors: state.temporaryConfiguration.activeMapping.mappingColors, + hideUnmappedIds: activeMappingInfo.hideUnmappedIds, + isMappingEnabled: activeMappingInfo.isMappingEnabled, + mapping: activeMappingInfo.mapping, + mappingName: activeMappingInfo.mappingName, + mappingType: activeMappingInfo.mappingType, + mappingColors: activeMappingInfo.mappingColors, activeViewport: state.viewModeData.plane.activeViewport, - segmentationLayer: getSegmentationLayer(state.dataset), + segmentationLayer: getSegmentationLayerByName(state.dataset, ownProps.layerName), activeCellId: getVolumeTracing(state.tracing) .map(tracing => tracing.activeCellId) .getOrElse(0), @@ -297,7 +302,7 @@ function mapStateToProps(state: OxalisState) { const debounceTime = 100; const maxWait = 500; -export default connect( +export default connect( mapStateToProps, mapDispatchToProps, )(debounceRender(MappingSettingsView, debounceTime, { maxWait })); 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 edf5cbf988a..795a24e5258 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 @@ -15,7 +15,11 @@ import { ControlModeEnum, type Vector3 } from "oxalis/constants"; import { convertToHybridTracing } from "admin/admin_rest_api"; import { formatScale } from "libs/format_utils"; import { getBaseVoxel } from "oxalis/model/scaleinfo"; -import { getDatasetExtentAsString, getResolutions } from "oxalis/model/accessors/dataset_accessor"; +import { + getDatasetExtentAsString, + getResolutions, + getVisibleSegmentationLayer, +} from "oxalis/model/accessors/dataset_accessor"; import { getCurrentResolution } from "oxalis/model/accessors/flycam_accessor"; import { getStats } from "oxalis/model/accessors/skeletontracing_accessor"; import { location } from "libs/window"; @@ -359,7 +363,12 @@ class DatasetInfoTabView extends React.PureComponent { handleConvertToHybrid = async () => { await Model.ensureSavedState(); - await convertToHybridTracing(this.props.tracing.annotationId); + + const maybeSegmentationLayer = getVisibleSegmentationLayer(Store.getState()); + await convertToHybridTracing( + this.props.tracing.annotationId, + maybeSegmentationLayer != null ? maybeSegmentationLayer.name : null, + ); location.reload(); }; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/export_bounding_box_modal.js b/frontend/javascripts/oxalis/view/right-border-tabs/export_bounding_box_modal.js index 7c7ee3b1a59..42fa392af97 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/export_bounding_box_modal.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/export_bounding_box_modal.js @@ -5,7 +5,7 @@ import type { BoundingBoxType } from "oxalis/constants"; import type { Tracing, AnnotationType } from "oxalis/store"; import type { APIDataset, APIDataLayer } from "types/api_flow_types"; import { startExportTiffJob } from "admin/admin_rest_api"; -import { getResolutionInfo } from "oxalis/model/accessors/dataset_accessor"; +import { getResolutionInfo, getMappingInfo } from "oxalis/model/accessors/dataset_accessor"; import Model from "oxalis/model"; import features from "features"; import * as Utils from "libs/utils"; @@ -37,18 +37,12 @@ const ExportBoundingBoxModal = ({ handleClose, dataset, boundingBox, tracing }: const volumeTracing = tracing != null ? tracing.volume : null; const annotationId = tracing != null ? tracing.annotationId : null; const annotationType = tracing != null ? tracing.annotationType : null; - const isMappingEnabled = useSelector( - state => state.temporaryConfiguration.activeMapping.isMappingEnabled, + const activeMappingInfos = useSelector( + state => state.temporaryConfiguration.activeMappingByLayer, ); - const hideUnmappedIds = useSelector( - state => state.temporaryConfiguration.activeMapping.hideUnmappedIds, - ); - const mappingName = useSelector(state => state.temporaryConfiguration.activeMapping.mappingName); - const mappingType = useSelector(state => state.temporaryConfiguration.activeMapping.mappingType); const isMergerModeEnabled = useSelector( state => state.temporaryConfiguration.isMergerModeEnabled, ); - const existsActivePersistentMapping = isMappingEnabled && !isMergerModeEnabled; const exportKey = (layerInfos: LayerInfos) => (layerInfos.layerName || "") + (layerInfos.tracingId || ""); @@ -75,8 +69,13 @@ const ExportBoundingBoxModal = ({ handleClose, dataset, boundingBox, tracing }: const hasMag1 = (layer: APIDataLayer) => getResolutionInfo(layer.resolutions).hasIndex(0); const allLayerInfos = dataset.dataSource.dataLayers.map(layer => { + const { isMappingEnabled, hideUnmappedIds, mappingName, mappingType } = getMappingInfo( + activeMappingInfos, + layer.name, + ); + const existsActivePersistentMapping = isMappingEnabled && !isMergerModeEnabled; const isColorLayer = layer.category === "color"; - if (layer.category === "color" || volumeTracing == null) + if (layer.category === "color" || !layer.isTracingLayer) { return { displayName: layer.name, layerName: layer.name, @@ -90,7 +89,14 @@ const ExportBoundingBoxModal = ({ handleClose, dataset, boundingBox, tracing }: mappingType: !isColorLayer && existsActivePersistentMapping ? mappingType : null, isColorLayer, }; - if (layer.fallbackLayerInfo != null) + } + // The layer is a volume tracing layer, since isTracingLayer is true. Therefore, a volumeTracing + // must exist. + if (volumeTracing == null) { + // Satisfy flow. + throw new Error("Volume tracing is null, but layer.isTracingLayer === true."); + } + if (layer.fallbackLayerInfo != null) { return { displayName: "Volume annotation with fallback segmentation", layerName: layer.fallbackLayerInfo.name, @@ -104,6 +110,7 @@ const ExportBoundingBoxModal = ({ handleClose, dataset, boundingBox, tracing }: mappingType: existsActivePersistentMapping ? mappingType : null, isColorLayer: false, }; + } return { displayName: "Volume annotation", layerName: null, @@ -119,24 +126,33 @@ const ExportBoundingBoxModal = ({ handleClose, dataset, boundingBox, tracing }: }; }); - const exportButtonsList = allLayerInfos.map(layerInfos => - layerInfos ? ( + const exportButtonsList = allLayerInfos.map(layerInfos => { + const parenthesesInfos = [ + startedExports.includes(exportKey(layerInfos)) ? "started" : null, + layerInfos.mappingName != null ? `using mapping "${layerInfos.mappingName}"` : null, + !layerInfos.hasMag1 ? "resolution 1 missing" : null, + ].filter(el => el); + const parenthesesInfosString = + parenthesesInfos.length > 0 ? ` (${parenthesesInfos.join(", ")})` : ""; + return layerInfos ? (

- ) : null, - ); + ) : null; + }); const dimensions = boundingBox.max.map((maxItem, index) => maxItem - boundingBox.min[index]); const volume = dimensions[0] * dimensions[1] * dimensions[2]; @@ -178,9 +194,7 @@ const ExportBoundingBoxModal = ({ handleClose, dataset, boundingBox, tracing }: let activeMappingMessage = null; if (isMergerModeEnabled) { activeMappingMessage = - "Exporting a volume layer does not export merger mode currently. Please disable merger mode before exporting data."; - } else if (isMappingEnabled) { - activeMappingMessage = `The active mapping ${mappingName} will be applied to the exported data.`; + "Exporting a volume layer does not export merger mode currently. Please disable merger mode before exporting data of the volume layer."; } return ( diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view.js b/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view.js index 41e0d8b2366..e18689e8ac6 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view.js @@ -15,10 +15,10 @@ import _ from "lodash"; import Toast from "libs/toast"; import type { ExtractReturn } from "libs/type_helpers"; -import type { RemoteMeshMetaData } from "types/api_flow_types"; -import type { OxalisState, IsosurfaceInformation } from "oxalis/store"; + +import type { APISegmentationLayer, APIUser, APIDataset } from "types/api_flow_types"; +import type { OxalisState, Flycam, IsosurfaceInformation } from "oxalis/store"; import Store from "oxalis/store"; -import Model from "oxalis/model"; import type { Vector3 } from "oxalis/constants"; import { createMeshFromBufferAction, @@ -27,29 +27,25 @@ import { triggerActiveIsosurfaceDownloadAction, triggerIsosurfaceDownloadAction, updateIsosurfaceVisibilityAction, - updateRemoteMeshMetaDataAction, removeIsosurfaceAction, refreshIsosurfaceAction, - addIsosurfaceAction, updateCurrentMeshFileAction, } from "oxalis/model/actions/annotation_actions"; import features from "features"; import { loadMeshFromFile, maybeFetchMeshFiles, + getBaseSegmentationName, } from "oxalis/view/right-border-tabs/meshes_view_helper"; import { getSegmentIdForPosition } from "oxalis/controller/combinations/volume_handlers"; import { updateDatasetSettingAction } from "oxalis/model/actions/settings_actions"; import { changeActiveIsosurfaceCellAction } from "oxalis/model/actions/segmentation_actions"; import { setPositionAction } from "oxalis/model/actions/flycam_actions"; +import { getPosition } from "oxalis/model/accessors/flycam_accessor"; import { - getPosition, - getRequestLogZoomStep, - getCurrentResolution, -} from "oxalis/model/accessors/flycam_accessor"; -import { - getSegmentationLayer, - getResolutionInfoOfSegmentationLayer, + getVisibleSegmentationLayer, + getResolutionInfoOfVisibleSegmentationLayer, + getMappingInfo, } from "oxalis/model/accessors/dataset_accessor"; import { isIsosurfaceStl } from "oxalis/model/sagas/isosurface_saga"; import { readFileAsArrayBuffer } from "libs/read_file"; @@ -75,47 +71,70 @@ export const stlIsosurfaceConstants = { // This file defines the component MeshesView. -const mapStateToProps = (state: OxalisState): * => ({ - meshes: state.tracing != null ? state.tracing.meshes : [], - isImporting: state.uiInformation.isImportingMesh, - isosurfaces: state.isosurfaces, - datasetConfiguration: state.datasetConfiguration, - dataset: state.dataset, - mappingColors: state.temporaryConfiguration.activeMapping.mappingColors, - flycam: state.flycam, - activeCellId: state.tracing.volume ? state.tracing.volume.activeCellId : null, - hasVolume: state.tracing.volume != null, - segmentationLayer: getSegmentationLayer(state.dataset), - zoomStep: getRequestLogZoomStep(state), - allowUpdate: state.tracing.restrictions.allowUpdate, - activeResolution: getCurrentResolution(state), - organization: state.dataset.owningOrganization, - datasetName: state.dataset.name, - availableMeshFiles: state.availableMeshFiles, - currentMeshFile: state.currentMeshFile, - activeUser: state.activeUser, -}); +type StateProps = {| + isImporting: boolean, + isosurfaces: { [segmentId: number]: IsosurfaceInformation }, + dataset: APIDataset, + mappingColors: ?Array, + flycam: Flycam, + hasVolume: boolean, + visibleSegmentationLayer: ?APISegmentationLayer, + allowUpdate: boolean, + organization: string, + datasetName: string, + availableMeshFiles: ?Array, + currentMeshFile: ?string, + activeUser: ?APIUser, +|}; + +const mapStateToProps = (state: OxalisState): StateProps => { + const visibleSegmentationLayer = getVisibleSegmentationLayer(state); + return { + isImporting: state.uiInformation.isImportingMesh, + isosurfaces: + visibleSegmentationLayer != null + ? state.isosurfacesByLayer[visibleSegmentationLayer.name] + : {}, + dataset: state.dataset, + mappingColors: getMappingInfo( + state.temporaryConfiguration.activeMappingByLayer, + visibleSegmentationLayer != null ? visibleSegmentationLayer.name : null, + ).mappingColors, + flycam: state.flycam, + hasVolume: state.tracing.volume != null, + visibleSegmentationLayer, + allowUpdate: state.tracing.restrictions.allowUpdate, + organization: state.dataset.owningOrganization, + datasetName: state.dataset.name, + availableMeshFiles: + visibleSegmentationLayer != null + ? state.availableMeshFilesByLayer[visibleSegmentationLayer.name] + : null, + currentMeshFile: + visibleSegmentationLayer != null + ? state.currentMeshFileByLayer[visibleSegmentationLayer.name] + : null, + activeUser: state.activeUser, + }; +}; const mapDispatchToProps = (dispatch: Dispatch<*>): * => ({ - updateRemoteMeshMetadata(id: string, meshMetaData: $Shape) { - dispatch(updateRemoteMeshMetaDataAction(id, meshMetaData)); - }, onChangeDatasetSettings(propertyName, value) { dispatch(updateDatasetSettingAction(propertyName, value)); }, deleteMesh(id: string) { dispatch(deleteMeshAction(id)); }, - onChangeVisibility(id, isVisible: boolean) { - dispatch(updateIsosurfaceVisibilityAction(id, isVisible)); + onChangeVisibility(layerName: string, id: number, isVisible: boolean) { + dispatch(updateIsosurfaceVisibilityAction(layerName, id, isVisible)); }, - async onStlUpload(info) { + async onStlUpload(layerName: string, info) { dispatch(setImportingMeshStateAction(true)); const buffer = await readFileAsArrayBuffer(info.file); if (isIsosurfaceStl(buffer)) { trackAction("Import Isosurface Mesh from STL"); - dispatch(importIsosurfaceFromStlAction(buffer)); + dispatch(importIsosurfaceFromStlAction(layerName, buffer)); } else { trackAction("Import STL"); dispatch(createMeshFromBufferAction(info.file.name, buffer)); @@ -130,19 +149,12 @@ const mapDispatchToProps = (dispatch: Dispatch<*>): * => ({ } dispatch(changeActiveIsosurfaceCellAction(cellId, seedPosition, shouldReload)); }, - addPrecomputedMesh(cellId, seedPosition) { - if (cellId == null) { - return; - } - dispatch(addIsosurfaceAction(cellId, seedPosition, true)); - }, - setCurrentMeshFile(fileName) { - dispatch(updateCurrentMeshFileAction(fileName)); + setCurrentMeshFile(layerName: string, fileName: string) { + dispatch(updateCurrentMeshFileAction(layerName, fileName)); }, }); type DispatchProps = ExtractReturn; -type StateProps = ExtractReturn; type Props = {| ...DispatchProps, ...StateProps |}; @@ -157,7 +169,7 @@ class MeshesView extends React.Component { }; componentDidMount() { - maybeFetchMeshFiles(this.props.segmentationLayer, this.props.dataset, false); + maybeFetchMeshFiles(this.props.visibleSegmentationLayer, this.props.dataset, false); if (features().jobsEnabled) { this.pollJobData(); } @@ -190,7 +202,7 @@ class MeshesView extends React.Component { this.setState({ activeMeshJobId: null }); // maybeFetchMeshFiles will fetch the new mesh file and also activate it if no other mesh file // currently exists. - maybeFetchMeshFiles(this.props.segmentationLayer, this.props.dataset, true); + maybeFetchMeshFiles(this.props.visibleSegmentationLayer, this.props.dataset, true); break; } case "STARTED": @@ -243,10 +255,11 @@ class MeshesView extends React.Component { "Meshes Computation is not supported for datasets that are not natively hosted on the server. Upload your dataset directly to weknossos.org to enable this feature."; } else if (this.props.hasVolume) { title = - this.props.segmentationLayer != null && this.props.segmentationLayer.fallbackLayer + this.props.visibleSegmentationLayer != null && + this.props.visibleSegmentationLayer.fallbackLayer ? "Meshes cannot be precomputed for volume annotations. However, you can open this dataset in view mode to precompute meshes for the dataset's segmentation layer." : "Meshes cannot be precomputed for volume annotations."; - } else if (this.props.segmentationLayer == null) { + } else if (this.props.visibleSegmentationLayer == null) { title = "There is no segmentation layer for which meshes could be precomputed."; } else { title = @@ -263,7 +276,7 @@ class MeshesView extends React.Component { getComputeMeshAdHocTooltipInfo = () => { let title = ""; let disabled = true; - if (this.props.segmentationLayer == null) { + if (this.props.visibleSegmentationLayer == null) { title = "There is no segmentation layer for which a mesh could be computed."; } else { title = "Compute mesh for the centered segment."; @@ -277,8 +290,6 @@ class MeshesView extends React.Component { }; getIsosurfaceList = () => { - const hasSegmentation = Model.getSegmentationLayer() != null; - const moveTo = (seedPosition: Vector3) => { Store.dispatch(setPositionAction(seedPosition, null, false)); }; @@ -297,7 +308,12 @@ class MeshesView extends React.Component { { - Store.dispatch(refreshIsosurfaceAction(segmentId)); + if (!this.props.visibleSegmentationLayer) { + return; + } + Store.dispatch( + refreshIsosurfaceAction(this.props.visibleSegmentationLayer.name, segmentId), + ); }} /> ); @@ -307,7 +323,12 @@ class MeshesView extends React.Component { { - Store.dispatch(refreshIsosurfaceAction(segmentId)); + if (!this.props.visibleSegmentationLayer) { + return; + } + Store.dispatch( + refreshIsosurfaceAction(this.props.visibleSegmentationLayer.name, segmentId), + ); }} /> @@ -319,7 +340,12 @@ class MeshesView extends React.Component { { - Store.dispatch(removeIsosurfaceAction(segmentId)); + if (!this.props.visibleSegmentationLayer) { + return; + } + Store.dispatch( + removeIsosurfaceAction(this.props.visibleSegmentationLayer.name, segmentId), + ); // reset the active mesh id so the deleted one is not reloaded immediately this.props.changeActiveIsosurfaceId(0, [0, 0, 0], false); }} @@ -336,7 +362,14 @@ class MeshesView extends React.Component { ) => { - this.props.onChangeVisibility(segmentId, event.target.checked); + if (!this.props.visibleSegmentationLayer) { + return; + } + this.props.onChangeVisibility( + this.props.visibleSegmentationLayer.name, + segmentId, + event.target.checked, + ); }} /> @@ -344,9 +377,8 @@ class MeshesView extends React.Component { const getIsosurfaceListItem = (isosurface: IsosurfaceInformation) => { const { segmentId, seedPosition, isLoading, isPrecomputed, isVisible } = isosurface; - const isCenteredCell = hasSegmentation - ? getSegmentIdForPosition(getPosition(this.props.flycam)) - : false; + const centeredCell = getSegmentIdForPosition(getPosition(this.props.flycam)); + const isHoveredItem = segmentId === this.state.hoveredListItem; const actionVisibility = isLoading || isHoveredItem ? "visible" : "hidden"; @@ -368,7 +400,7 @@ class MeshesView extends React.Component {
{isHoveredItem ? ( @@ -411,7 +443,7 @@ class MeshesView extends React.Component { render() { const startComputingMeshfile = async () => { - const datasetResolutionInfo = getResolutionInfoOfSegmentationLayer(this.props.dataset); + const datasetResolutionInfo = getResolutionInfoOfVisibleSegmentationLayer(Store.getState()); const defaultOrHigherIndex = datasetResolutionInfo.getIndexOrClosestHigherIndex( defaultMeshfileGenerationResolutionIndex, ); @@ -425,11 +457,11 @@ class MeshesView extends React.Component { meshfileResolutionIndex, ); - if (this.props.segmentationLayer != null) { + if (this.props.visibleSegmentationLayer != null) { const job = await startComputeMeshFileJob( this.props.organization, this.props.datasetName, - this.props.segmentationLayer.fallbackLayer || this.props.segmentationLayer.name, + getBaseSegmentationName(this.props.visibleSegmentationLayer), meshfileResolution, ); this.setState({ activeMeshJobId: job.id }); @@ -473,7 +505,10 @@ class MeshesView extends React.Component { accept=".stl" beforeUpload={() => false} onChange={file => { - this.props.onStlUpload(file); + if (!this.props.visibleSegmentationLayer) { + return; + } + this.props.onStlUpload(this.props.visibleSegmentationLayer.name, file); }} showUploadList={false} style={{ fontSize: 16, cursor: "pointer" }} @@ -512,23 +547,23 @@ class MeshesView extends React.Component { Toast.info("No segment found at centered position"); return; } - if (!this.props.currentMeshFile || !this.props.segmentationLayer) { + if (!this.props.currentMeshFile || !this.props.visibleSegmentationLayer) { return; } await loadMeshFromFile( id, pos, this.props.currentMeshFile, - this.props.segmentationLayer, + this.props.visibleSegmentationLayer, this.props.dataset, ); }; const handleMeshFileSelected = async mesh => { if (mesh.key === "refresh") { - maybeFetchMeshFiles(this.props.segmentationLayer, this.props.dataset, true); - } else { - this.props.setCurrentMeshFile(mesh.key); + maybeFetchMeshFiles(this.props.visibleSegmentationLayer, this.props.dataset, true); + } else if (this.props.visibleSegmentationLayer != null) { + this.props.setCurrentMeshFile(this.props.visibleSegmentationLayer.name, mesh.key); loadPrecomputedMesh(); } }; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view_helper.js b/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view_helper.js index e07fcca13f4..12927eecdf4 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view_helper.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view_helper.js @@ -23,6 +23,10 @@ import processTaskWithPool from "libs/task_pool"; const PARALLEL_MESH_LOADING_COUNT = 6; +export function getBaseSegmentationName(segmentationLayer: APIDataLayer) { + return segmentationLayer.fallbackLayer || segmentationLayer.name; +} + export async function maybeFetchMeshFiles( segmentationLayer: ?APIDataLayer, dataset: APIDataset, @@ -32,20 +36,24 @@ export async function maybeFetchMeshFiles( if (!segmentationLayer) { return []; } - const files = Store.getState().availableMeshFiles; + const layerName = segmentationLayer.name; + const files = Store.getState().availableMeshFilesByLayer[layerName]; // only send new get request, if it hasn't happened before (files in store are null) // else return the stored files (might be empty array). Or if we force a reload. if (!files || mustRequest) { - const layerName = segmentationLayer.fallbackLayer || segmentationLayer.name; const availableMeshFiles = await getMeshfilesForDatasetLayer( dataset.dataStore.url, dataset, - layerName, + getBaseSegmentationName(segmentationLayer), ); - Store.dispatch(updateMeshFileListAction(availableMeshFiles)); - if (!Store.getState().currentMeshFile && availableMeshFiles.length > 0 && autoActivate) { - Store.dispatch(updateCurrentMeshFileAction(availableMeshFiles[0])); + Store.dispatch(updateMeshFileListAction(layerName, availableMeshFiles)); + if ( + !Store.getState().currentMeshFileByLayer[layerName] && + availableMeshFiles.length > 0 && + autoActivate + ) { + Store.dispatch(updateCurrentMeshFileAction(layerName, availableMeshFiles[0])); } return availableMeshFiles; } @@ -59,16 +67,16 @@ export async function loadMeshFromFile( segmentationLayer: APIDataLayer, dataset: APIDataset, ): Promise { - Store.dispatch(addIsosurfaceAction(id, pos, true)); - Store.dispatch(startedLoadingIsosurfaceAction(id)); + const layerName = segmentationLayer.name; + Store.dispatch(addIsosurfaceAction(layerName, id, pos, true)); + Store.dispatch(startedLoadingIsosurfaceAction(layerName, id)); - const layerName = segmentationLayer.fallbackLayer || segmentationLayer.name; let availableChunks = null; try { availableChunks = await getMeshfileChunksForSegment( dataset.dataStore.url, dataset, - layerName, + getBaseSegmentationName(segmentationLayer), fileName, id, ); @@ -76,13 +84,13 @@ export async function loadMeshFromFile( console.warn("Mesh chunk couldn't be loaded due to", exception); Toast.warning(messages["tracing.mesh_listing_failed"]); - Store.dispatch(finishedLoadingIsosurfaceAction(id)); - Store.dispatch(removeIsosurfaceAction(id)); + Store.dispatch(finishedLoadingIsosurfaceAction(layerName, id)); + Store.dispatch(removeIsosurfaceAction(layerName, id)); return; } const tasks = availableChunks.map(chunkPos => async () => { - if (Store.getState().isosurfaces[id] == null) { + if (Store.getState().isosurfacesByLayer[layerName][id] == null) { // Don't load chunk, since the mesh seems to have been deleted in the meantime (e.g., by the user). return; } @@ -95,7 +103,7 @@ export async function loadMeshFromFile( id, chunkPos, ); - if (Store.getState().isosurfaces[id] == null) { + if (Store.getState().isosurfacesByLayer[layerName][id] == null) { // Don't add chunks, since the mesh seems to have been deleted in the meantime (e.g., by the user). return; } @@ -109,10 +117,10 @@ export async function loadMeshFromFile( Toast.warning("Some mesh objects could not be loaded."); } - if (Store.getState().isosurfaces[id] == null) { + if (Store.getState().isosurfacesByLayer[layerName][id] == null) { // The mesh was removed from the store in the mean time. Don't do anything. return; } - Store.dispatch(finishedLoadingIsosurfaceAction(id)); + Store.dispatch(finishedLoadingIsosurfaceAction(layerName, id)); } diff --git a/frontend/javascripts/oxalis/view/statusbar.js b/frontend/javascripts/oxalis/view/statusbar.js index acafce444a0..e4969abc805 100644 --- a/frontend/javascripts/oxalis/view/statusbar.js +++ b/frontend/javascripts/oxalis/view/statusbar.js @@ -8,7 +8,7 @@ import React from "react"; import { WarningOutlined, MoreOutlined } from "@ant-design/icons"; import { type Vector3, OrthoViews } from "oxalis/constants"; -import { getSegmentationLayer } from "oxalis/model/accessors/dataset_accessor"; +import { getVisibleSegmentationLayer } from "oxalis/model/accessors/dataset_accessor"; import { NumberInputPopoverSetting } from "oxalis/view/components/setting_input_views"; import { useKeyPress } from "libs/react_hooks"; @@ -36,8 +36,6 @@ const lineColor = "rgba(255, 255, 255, 0.67)"; const moreIconStyle = { height: 14, color: lineColor }; const moreLinkStyle = { marginLeft: 10, marginRight: "auto" }; -const hasSegmentation = () => Model.getSegmentationLayer() != null; - function getPosString(pos: Vector3) { return V3.floor(pos).join(","); } @@ -224,8 +222,6 @@ function ShortcutsInfo() { } function getCellInfo(globalMousePosition: ?Vector3) { - if (!hasSegmentation()) return null; - const getSegmentIdString = () => { const hoveredCellInfo = Model.getHoveredCellId(globalMousePosition); if (!hoveredCellInfo) { @@ -271,8 +267,9 @@ function Infos() { const onChangeActiveNodeId = id => dispatch(setActiveNodeAction(id)); const onChangeActiveTreeId = id => dispatch(setActiveTreeAction(id)); + const hasVisibleSegmentation = useSelector(state => getVisibleSegmentationLayer(state) != null); const hasUint64Segmentation = useSelector(state => { - const segmentationLayer = getSegmentationLayer(state.dataset); + const segmentationLayer = getVisibleSegmentationLayer(state); return segmentationLayer ? segmentationLayer.originalElementClass === "uint64" : false; }); const globalMousePosition = useSelector(state => { @@ -299,7 +296,7 @@ function Infos() { Pos [{globalMousePosition ? getPosString(globalMousePosition) : "-,-,-"}] ) : null} - {isPlaneMode ? getCellInfo(globalMousePosition) : null} + {isPlaneMode && hasVisibleSegmentation ? getCellInfo(globalMousePosition) : null} {isVolumeAnnotation ? ( diff --git a/frontend/javascripts/oxalis/view/version_list.js b/frontend/javascripts/oxalis/view/version_list.js index a6a90e37d74..ec30f33512e 100644 --- a/frontend/javascripts/oxalis/view/version_list.js +++ b/frontend/javascripts/oxalis/view/version_list.js @@ -53,7 +53,7 @@ export async function previewVersion(versions?: Versions) { await api.tracing.restart(annotationType, annotationId, ControlModeEnum.TRACE, versions); Store.dispatch(setAnnotationAllowUpdateAction(false)); - const segmentationLayer = Model.getSegmentationLayer(); + const segmentationLayer = Model.getSegmentationTracingLayer(); const shouldPreviewVolumeVersion = versions != null && versions.volume != null; const shouldPreviewNewestVersion = versions == null; if (segmentationLayer != null && (shouldPreviewVolumeVersion || shouldPreviewNewestVersion)) { diff --git a/frontend/javascripts/router.js b/frontend/javascripts/router.js index f077939ecd8..df2bb4941c7 100644 --- a/frontend/javascripts/router.js +++ b/frontend/javascripts/router.js @@ -466,7 +466,7 @@ class ReactRouter extends React.Component { /> ( { if ( !match.params.organizationName || !match.params.dataSetName || - !match.params.type || - !match.params.withFallback + !match.params.type ) { // Typehint for flow throw new Error("Invalid URL"); @@ -488,19 +487,19 @@ class ReactRouter extends React.Component { const type = Enum.coalesce(TracingTypeEnum, match.params.type) || TracingTypeEnum.skeleton; - const withFallback = match.params.withFallback === "true"; - const params = Utils.getUrlParamsObjectFromString(location.search); + const getParams = Utils.getUrlParamsObjectFromString(location.search); + const { fallbackLayerName } = getParams; const resolutionRestrictions = {}; - if (params.minRes !== undefined) { - resolutionRestrictions.min = parseInt(params.minRes); + if (getParams.minRes !== undefined) { + resolutionRestrictions.min = parseInt(getParams.minRes); if (!_.isNumber(resolutionRestrictions.min)) { throw new Error("Invalid minRes parameter"); } } - if (params.maxRes !== undefined) { - resolutionRestrictions.max = parseInt(params.maxRes); + if (getParams.maxRes !== undefined) { + resolutionRestrictions.max = parseInt(getParams.maxRes); if (!_.isNumber(resolutionRestrictions.max)) { throw new Error("Invalid maxRes parameter"); } @@ -508,7 +507,7 @@ class ReactRouter extends React.Component { const annotation = await createExplorational( dataset, type, - withFallback, + fallbackLayerName, resolutionRestrictions, ); trackAction(`Create ${type} tracing`); diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.js b/frontend/javascripts/test/api/api_skeleton_latest.spec.js index ce44cf0b11e..abb285a5596 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.js +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.js @@ -97,12 +97,12 @@ test("Data Api: setMapping should throw an error if the layer name is not valid" test("Data Api: setMapping should set a mapping of a layer", t => { const { api, model } = t.context; const cube = model.getCubeByLayerName("segmentation"); - t.is(Store.getState().temporaryConfiguration.activeMapping.mapping, null); + t.is(Store.getState().temporaryConfiguration.activeMappingByLayer.segmentation.mapping, null); api.data.setMapping("segmentation", [1, 3]); - t.not(Store.getState().temporaryConfiguration.activeMapping.mapping, null); + t.not(Store.getState().temporaryConfiguration.activeMappingByLayer.segmentation.mapping, null); // Workaround: This is usually called after the mapping textures were created successfully // and can be rendered, which doesn't happen in this test scenario - Store.dispatch(setMappingEnabledAction(true)); + Store.dispatch(setMappingEnabledAction("segmentation", true)); t.is(cube.mapId(1), 3); }); diff --git a/frontend/javascripts/test/api/api_v2.spec.js b/frontend/javascripts/test/api/api_v2.spec.js index 6b89f70d5c0..9a88cdf2f19 100644 --- a/frontend/javascripts/test/api/api_v2.spec.js +++ b/frontend/javascripts/test/api/api_v2.spec.js @@ -71,12 +71,12 @@ test("setMapping should throw an error if the layer name is not valid", t => { test("setMapping should set a mapping of a layer", t => { const { api, model } = t.context; const cube = model.getCubeByLayerName("segmentation"); - t.is(Store.getState().temporaryConfiguration.activeMapping.mapping, null); + t.is(Store.getState().temporaryConfiguration.activeMappingByLayer.segmentation.mapping, null); api.data.setMapping("segmentation", [1, 3]); - t.not(Store.getState().temporaryConfiguration.activeMapping.mapping, null); + t.not(Store.getState().temporaryConfiguration.activeMappingByLayer.segmentation.mapping, null); // Workaround: This is usually called after the mapping textures were created successfully // and can be rendered, which doesn't happen in this test scenario - Store.dispatch(setMappingEnabledAction(true)); + Store.dispatch(setMappingEnabledAction("segmentation", true)); t.is(cube.mapId(1), 3); }); diff --git a/frontend/javascripts/test/api/api_volume_latest.spec.js b/frontend/javascripts/test/api/api_volume_latest.spec.js index d031bffa1b5..51a64ed5ccd 100644 --- a/frontend/javascripts/test/api/api_volume_latest.spec.js +++ b/frontend/javascripts/test/api/api_volume_latest.spec.js @@ -42,7 +42,7 @@ test("setAnnotationTool should throw an error for an invalid tool", t => { test("Data API: labelVoxels should label a list of voxels", t => { const { api, model } = t.context; - const { cube } = model.getSegmentationLayer(); + const { cube } = model.getSegmentationTracingLayer(); api.data.labelVoxels([[1, 2, 3], [7, 8, 9]], 34); // The specified voxels should be labeled with the new value diff --git a/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.js b/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.js index 84657ab177f..0e7b60ff76f 100644 --- a/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.js +++ b/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.js @@ -139,7 +139,7 @@ test.serial("finishAllAnnotations()", async t => { }); test.serial("createExplorational() and finishAnnotation()", async t => { - const createdExplorational = await api.createExplorational(dataSetId, "skeleton", false); + const createdExplorational = await api.createExplorational(dataSetId, "skeleton", null); t.snapshot(replaceVolatileValues(createdExplorational), { id: "annotations-createExplorational", @@ -155,7 +155,7 @@ test.serial("createExplorational() and finishAnnotation()", async t => { }); test.serial("getTracingForAnnotations()", async t => { - const createdExplorational = await api.createExplorational(dataSetId, "skeleton", false); + const createdExplorational = await api.createExplorational(dataSetId, "skeleton", null); const tracing = await api.getTracingForAnnotations(createdExplorational); writeFlowCheckingFile(tracing, "tracing", "HybridServerTracing"); @@ -165,7 +165,7 @@ test.serial("getTracingForAnnotations()", async t => { }); test.serial("getTracingForAnnotations() for volume", async t => { - const createdExplorational = await api.createExplorational(dataSetId, "volume", false); + const createdExplorational = await api.createExplorational(dataSetId, "volume", null); const tracing = await api.getTracingForAnnotations(createdExplorational); writeFlowCheckingFile(tracing, "tracing-volume", "HybridServerTracing"); @@ -175,7 +175,7 @@ test.serial("getTracingForAnnotations() for volume", async t => { }); test.serial("getTracingForAnnotations() for hybrid", async t => { - const createdExplorational = await api.createExplorational(dataSetId, "hybrid", false); + const createdExplorational = await api.createExplorational(dataSetId, "hybrid", null); const tracing = await api.getTracingForAnnotations(createdExplorational); writeFlowCheckingFile(tracing, "tracing-hybrid", "HybridServerTracing"); @@ -205,7 +205,7 @@ async function sendUpdateActions(explorational, queue) { } test.serial("Send update actions and compare resulting tracing", async t => { - const createdExplorational = await api.createExplorational(dataSetId, "skeleton", false); + const createdExplorational = await api.createExplorational(dataSetId, "skeleton", null); const initialSkeleton = { activeNodeId: undefined, userBoundingBoxes: [] }; const saveQueue = addVersionNumbers( @@ -226,7 +226,7 @@ test.serial("Send update actions and compare resulting tracing", async t => { }); test("Send complex update actions and compare resulting tracing", async t => { - const createdExplorational = await api.createExplorational(dataSetId, "skeleton", false); + const createdExplorational = await api.createExplorational(dataSetId, "skeleton", null); const trees = createTreeMapFromTreeArray(generateDummyTrees(5, 5)); const treeGroups = [ diff --git a/frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.js b/frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.js index 1ace8bf3fed..5aadf060518 100644 --- a/frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.js +++ b/frontend/javascripts/test/model/binary/layers/wkstore_adapter.spec.js @@ -41,7 +41,7 @@ const StoreMock = { }, datasetConfiguration: { fourBit: _fourBit }, temporaryConfiguration: { - activeMapping: {}, + activeMappingByLayer: {}, }, }), dispatch: sinon.stub(), diff --git a/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js b/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js index a7a527cd97a..15e3022fc78 100644 --- a/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js +++ b/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js @@ -42,7 +42,7 @@ export async function screenshotDataset( const createdExplorational = await createExplorational( datasetId, "skeleton", - false, + null, null, options, ); @@ -63,7 +63,7 @@ export async function screenshotDatasetWithMapping( const createdExplorational = await createExplorational( datasetId, "skeleton", - false, + null, null, options, ); diff --git a/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js b/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js index 8fff36bda4d..430d8c088b2 100644 --- a/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js +++ b/frontend/javascripts/test/reducers/volumetracing_reducer.spec.js @@ -65,6 +65,7 @@ const initialState = update(defaultState, { resolutions: [[1, 1, 1], [2, 2, 2], [4, 4, 4]], category: "segmentation", name: "segmentation", + isTracingLayer: true, }, ], }, diff --git a/frontend/javascripts/test/shaders/shader_syntax.spec.js b/frontend/javascripts/test/shaders/shader_syntax.spec.js index a5f038e0926..5194b44e978 100644 --- a/frontend/javascripts/test/shaders/shader_syntax.spec.js +++ b/frontend/javascripts/test/shaders/shader_syntax.spec.js @@ -13,8 +13,7 @@ test("Shader syntax: Ortho Mode", t => { const code = getMainFragmentShader({ colorLayerNames: ["color_layer_1", "color_layer_2"], packingDegreeLookup: { color_layer_1: 4.0, color_layer_2: 4.0 }, - hasSegmentation: false, - segmentationName: "", + segmentationLayerNames: [], isMappingSupported: true, dataTextureCountPerLayer: 3, resolutions, @@ -33,8 +32,7 @@ test("Shader syntax: Ortho Mode + Segmentation - Mapping", t => { const code = getMainFragmentShader({ colorLayerNames: ["color_layer_1", "color_layer_2"], packingDegreeLookup: { color_layer_1: 4.0, color_layer_2: 4.0, segmentationLayer: 1.0 }, - hasSegmentation: true, - segmentationName: "segmentationLayer", + segmentationLayerNames: ["segmentationLayer"], isMappingSupported: false, dataTextureCountPerLayer: 3, resolutions, @@ -53,8 +51,7 @@ test("Shader syntax: Ortho Mode + Segmentation + Mapping", t => { const code = getMainFragmentShader({ colorLayerNames: ["color_layer_1", "color_layer_2"], packingDegreeLookup: { color_layer_1: 4.0, color_layer_2: 4.0, segmentationLayer: 1.0 }, - hasSegmentation: true, - segmentationName: "segmentationLayer", + segmentationLayerNames: ["segmentationLayer"], isMappingSupported: true, dataTextureCountPerLayer: 3, resolutions, @@ -73,8 +70,7 @@ test("Shader syntax: Arbitrary Mode (no segmentation available)", t => { const code = getMainFragmentShader({ colorLayerNames: ["color_layer_1", "color_layer_2"], packingDegreeLookup: { color_layer_1: 4.0, color_layer_2: 4.0 }, - hasSegmentation: false, - segmentationName: "", + segmentationLayerNames: [], isMappingSupported: true, dataTextureCountPerLayer: 3, resolutions, @@ -93,8 +89,7 @@ test("Shader syntax: Arbitrary Mode (segmentation available)", t => { const code = getMainFragmentShader({ colorLayerNames: ["color_layer_1", "color_layer_2"], packingDegreeLookup: { color_layer_1: 4.0, color_layer_2: 4.0, segmentationLayer: 1.0 }, - hasSegmentation: true, - segmentationName: "segmentationLayer", + segmentationLayerNames: ["segmentationLayer"], isMappingSupported: true, dataTextureCountPerLayer: 3, resolutions, @@ -113,8 +108,7 @@ test("Shader syntax: Ortho Mode (rgb and float layer)", t => { const code = getMainFragmentShader({ colorLayerNames: ["color_layer_1", "color_layer_2"], packingDegreeLookup: { color_layer_1: 1.0, color_layer_2: 4.0 }, - hasSegmentation: false, - segmentationName: "", + segmentationLayerNames: [], isMappingSupported: true, dataTextureCountPerLayer: 3, resolutions, diff --git a/frontend/javascripts/types/api_flow_types.js b/frontend/javascripts/types/api_flow_types.js index 4194b57ec14..05768b8572a 100644 --- a/frontend/javascripts/types/api_flow_types.js +++ b/frontend/javascripts/types/api_flow_types.js @@ -60,6 +60,7 @@ export type APISegmentationLayer = {| +fallbackLayer?: ?string, // eslint-disable-next-line no-use-before-define +fallbackLayerInfo?: APIDataLayer, + +isTracingLayer?: boolean, |}; export type APIDataLayer = APIColorLayer | APISegmentationLayer; diff --git a/package.json b/package.json index 158acaa4e5a..c61cd0e900c 100644 --- a/package.json +++ b/package.json @@ -192,7 +192,7 @@ "redux-saga": "^1.0.0", "resumablejs": "^1.1.0", "saxophone": "^0.7.1", - "three": "^0.106.0", + "three": "^0.110.0", "tween.js": "^16.3.1", "url-join": "^4.0.0", "worker-loader": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 933d741424a..bea1d075628 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12701,10 +12701,10 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -three@^0.106.0: - version "0.106.2" - resolved "https://registry.yarnpkg.com/three/-/three-0.106.2.tgz#6752d304c470e4df230944ecd45325e7bf79e1f8" - integrity sha512-4Tlx43uoxnIaZFW2Bzkd1rXsatvVHEWAZJy8LuE+s6Q8c66ogNnhfq1bHiBKPAnXP230LD11H/ScIZc2LZMviA== +three@^0.110.0: + version "0.110.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.110.0.tgz#8719591de1336269113ee79f4268ba247b2324f8" + integrity sha512-wlurH8XBO9Sd5VIw8nBa+taLR20kqaI4e9FiuMh6tqK8eOS2q2R+ZoUyufbyDTVTHhs8GiTbv0r2CMLkwerFJg== through2-filter@^3.0.0: version "3.0.0"