diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index bb8a6ea7983..c7323ee4499 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -19,6 +19,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Fixed - Fix that active segment and node id were not shown in status bar when being in a non-hybrid annotation. [#5638](https://github.com/scalableminds/webknossos/pull/5638) +- Fix that "Compute Mesh File" button was enabled in scenarios where it should not be supported (e.g., when no segmentation layer exists). [#5648](https://github.com/scalableminds/webknossos/pull/5648) - Fixed a bug where an authentication error was shown when viewing the meshes tab while not logged in. [#5647](https://github.com/scalableminds/webknossos/pull/5647) ### Removed diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index d4db81740de..11cab4e53f6 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -1565,14 +1565,14 @@ export function computeIsosurface( { data: { // The back-end needs a small padding at the border of the - // bounding box to calculate the isosurface. This padding + // 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)), zoomStep, - // Segment to build isosurface for + // Segment to build mesh for segmentId, - // Name of mapping to apply before building isosurface (optional) + // Name of mapping to apply before building mesh (optional) mapping: layer.activeMapping, mappingType: layer.activeMappingType, // "size" of each voxel (i.e., only every nth voxel is considered in each dimension) diff --git a/frontend/javascripts/messages.js b/frontend/javascripts/messages.js index cbf1253ee3c..c44bd77e0fe 100644 --- a/frontend/javascripts/messages.js +++ b/frontend/javascripts/messages.js @@ -155,10 +155,8 @@ instead. Only enable this option if you understand its effect. All layers will n "A corruption in the current skeleton annotation was detected. Please contact your supervisor and/or the maintainers of webKnossos to get help for restoring a working version. Please include as much details as possible about your past user interactions. This will be very helpful to investigate the source of this bug.", "tracing.merger_mode_node_outside_segment": "You cannot place nodes outside of a segment in merger mode.", - "tracing.not_isosurface_available_to_download": [ - "There is no isosurface for the active segment id available to download.", - 'Click with "CTRL + Left Mouse" on the desired segment to load it\'s isosurface.', - ], + "tracing.not_isosurface_available_to_download": + "There is no mesh for the active segment id available to download.", "tracing.mesh_listing_failed": "A precomputed mesh could not be loaded for this segment. More information was printed to the browser's console.", "tracing.confirm_remove_fallback_layer.title": diff --git a/frontend/javascripts/oxalis/controller/td_controller.js b/frontend/javascripts/oxalis/controller/td_controller.js index 39e1ea2a38e..0d412cb8f98 100644 --- a/frontend/javascripts/oxalis/controller/td_controller.js +++ b/frontend/javascripts/oxalis/controller/td_controller.js @@ -197,7 +197,7 @@ class TDController extends React.PureComponent { } if (!event.shiftKey && !event.ctrlKey) { - // No modifiers were pressed. No isosurface related action is necessary. + // No modifiers were pressed. No mesh related action is necessary. return; } diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index 60423287279..8d10ea49ad7 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -328,7 +328,7 @@ function* maybeLoadIsosurface( } catch (exception) { retryCount++; ErrorHandling.notify(exception); - console.warn("Retrying isosurface generation..."); + console.warn("Retrying mesh generation..."); yield* call(sleep, RETRY_WAIT_TIME * 2 ** retryCount); } } @@ -339,8 +339,8 @@ function* downloadIsosurfaceCellById(cellId: number): Saga { const sceneController = getSceneController(); const geometry = sceneController.getIsosurfaceGeometry(cellId); if (geometry == null) { - const errorMessages = messages["tracing.not_isosurface_available_to_download"]; - Toast.error(errorMessages[0], { sticky: false }, errorMessages[1]); + const errorMessage = messages["tracing.not_isosurface_available_to_download"]; + Toast.error(errorMessage, { sticky: false }); return; } const stl = exportToStl(geometry); @@ -353,7 +353,7 @@ function* downloadIsosurfaceCellById(cellId: number): Saga { stl.setUint32(cellIdIndex, cellId, true); const blob = new Blob([stl]); - yield* call(saveAs, blob, `isosurface-${cellId}.stl`); + yield* call(saveAs, blob, `mesh-${cellId}.stl`); } function* downloadIsosurfaceCell(action: TriggerIsosurfaceDownloadAction): Saga { 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 41f14c03aec..405e4041464 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view.js +++ b/frontend/javascripts/oxalis/view/right-border-tabs/meshes_view.js @@ -84,6 +84,7 @@ const mapStateToProps = (state: OxalisState): * => ({ 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, @@ -230,81 +231,62 @@ class MeshesView extends React.Component { this.intervalID = setTimeout(() => this.pollJobData(), refreshInterval); } - render() { - const hasSegmentation = Model.getSegmentationLayer() != null; + getPrecomputeMeshesTooltipInfo = () => { + let title = ""; + let disabled = true; + if (!features().jobsEnabled) { + title = "Computation jobs are not enabled for this webKnossos instance."; + } else if (this.props.activeUser == null) { + title = "Please log in to precompute the meshes of this dataset."; + } else if (this.props.hasVolume) { + title = + this.props.segmentationLayer != null && this.props.segmentationLayer.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) { + title = "There is no segmentation layer for which meshes could be precomputed."; + } else { + title = + "Precompute meshes for all segments of this dataset so that meshes for segments can be loaded quickly afterwards from a mesh file."; + disabled = false; + } - const moveTo = (seedPosition: Vector3) => { - Store.dispatch(setPositionAction(seedPosition, null, false)); + return { + disabled, + title, }; + }; - const startComputingMeshfile = async () => { - const datasetResolutionInfo = getResolutionInfoOfSegmentationLayer(this.props.dataset); - const defaultOrHigherIndex = datasetResolutionInfo.getIndexOrClosestHigherIndex( - defaultMeshfileGenerationResolutionIndex, - ); - - const meshfileResolutionIndex = - defaultOrHigherIndex != null - ? defaultOrHigherIndex - : datasetResolutionInfo.getClosestExistingIndex(defaultMeshfileGenerationResolutionIndex); - - const meshfileResolution = datasetResolutionInfo.getResolutionByIndexWithFallback( - meshfileResolutionIndex, - ); + getComputeMeshAdHocTooltipInfo = () => { + let title = ""; + let disabled = true; + if (this.props.hasVolume) { + title = + this.props.segmentationLayer != null && this.props.segmentationLayer.fallbackLayer + ? "Meshes cannot be computed for volume annotations. However, you can open this dataset in view mode to compute meshes for the dataset's segmentation layer." + : "Meshes cannot be computed for volume annotations."; + } else if (this.props.segmentationLayer == null) { + title = "There is no segmentation layer for which a mesh could be computed."; + } else { + title = "Compute mesh for the centered segment."; + disabled = false; + } - if (this.props.segmentationLayer != null) { - const job = await startComputeMeshFileJob( - this.props.organization, - this.props.datasetName, - this.props.segmentationLayer.fallbackLayer || this.props.segmentationLayer.name, - meshfileResolution, - ); - this.setState({ activeMeshJobId: job.id }); - Toast.info( - - The computation of a mesh file was started. For large datasets, this may take a while. - Closing this tab will not stop the computation. -
- See{" "} - - Processing Jobs - {" "} - for an overview of running jobs. -
, - ); - } else { - Toast.error( - "The computation of a mesh file could not be started because no segmentation layer was found.", - ); - } + return { + disabled, + title, }; + }; - const getComputeMeshfileTooltip = node => ( - - {node} - - ); + getIsosurfaceList = () => { + const hasSegmentation = Model.getSegmentationLayer() != null; - const getComputeMeshFileButton = () => - getComputeMeshfileTooltip( - , - ); + const moveTo = (seedPosition: Vector3) => { + Store.dispatch(setPositionAction(seedPosition, null, false)); + }; const getDownloadButton = (segmentId: number) => ( - + Store.dispatch(triggerIsosurfaceDownloadAction(segmentId))} @@ -323,7 +305,7 @@ class MeshesView extends React.Component { ); } else { return isPrecomputed ? null : ( - + { @@ -335,12 +317,12 @@ class MeshesView extends React.Component { } }; const getDeleteButton = (segmentId: number) => ( - + { Store.dispatch(removeIsosurfaceAction(segmentId)); - // reset the active isosurface id so the deleted one is not reloaded immediately + // reset the active mesh id so the deleted one is not reloaded immediately this.props.changeActiveIsosurfaceId(0, [0, 0, 0], false); }} /> @@ -351,6 +333,143 @@ class MeshesView extends React.Component { const convertCellIdToCSS = id => convertHSLAToCSSString(jsConvertCellIdToHSLA(id, this.props.mappingColors)); + const getToggleVisibilityCheckbox = (segmentId: number, isVisible: boolean) => ( + + ) => { + this.props.onChangeVisibility(segmentId, event.target.checked); + }} + /> + + ); + + const getIsosurfaceListItem = (isosurface: IsosurfaceInformation) => { + const { segmentId, seedPosition, isLoading, isPrecomputed, isVisible } = isosurface; + const isCenteredCell = hasSegmentation + ? getSegmentIdForPosition(getPosition(this.props.flycam)) + : false; + const isHoveredItem = segmentId === this.state.hoveredListItem; + const actionVisibility = isLoading || isHoveredItem ? "visible" : "hidden"; + + const textStyle = isVisible ? {} : { fontStyle: "italic", color: "#989898" }; + return ( + { + this.setState({ hoveredListItem: segmentId }); + }} + onMouseLeave={() => { + this.setState({ hoveredListItem: null }); + }} + key={segmentId} + > +
+
+ {isHoveredItem ? ( + getToggleVisibilityCheckbox(segmentId, isVisible) + ) : ( + + )}{" "} + { + this.props.changeActiveIsosurfaceId(segmentId, seedPosition, !isPrecomputed); + moveTo(seedPosition); + }} + style={textStyle} + > + Segment {segmentId} + +
+
+ {getRefreshButton(segmentId, isPrecomputed, isLoading)} + {getDownloadButton(segmentId)} + {getDeleteButton(segmentId)} +
+
+
+ ); + }; + + // $FlowIgnore[incompatible-call] + // $FlowIgnore[missing-annot] flow does not know that the values passed as isosurfaces are indeed from the type IsosurfaceInformation + return Object.values(this.props.isosurfaces).map((isosurface: IsosurfaceInformation) => + getIsosurfaceListItem(isosurface), + ); + }; + + render() { + const startComputingMeshfile = async () => { + const datasetResolutionInfo = getResolutionInfoOfSegmentationLayer(this.props.dataset); + const defaultOrHigherIndex = datasetResolutionInfo.getIndexOrClosestHigherIndex( + defaultMeshfileGenerationResolutionIndex, + ); + + const meshfileResolutionIndex = + defaultOrHigherIndex != null + ? defaultOrHigherIndex + : datasetResolutionInfo.getClosestExistingIndex(defaultMeshfileGenerationResolutionIndex); + + const meshfileResolution = datasetResolutionInfo.getResolutionByIndexWithFallback( + meshfileResolutionIndex, + ); + + if (this.props.segmentationLayer != null) { + const job = await startComputeMeshFileJob( + this.props.organization, + this.props.datasetName, + this.props.segmentationLayer.fallbackLayer || this.props.segmentationLayer.name, + meshfileResolution, + ); + this.setState({ activeMeshJobId: job.id }); + Toast.info( + + The computation of a mesh file was started. For large datasets, this may take a while. + Closing this tab will not stop the computation. +
+ See{" "} + + Processing Jobs + {" "} + for an overview of running jobs. +
, + ); + } else { + Toast.error( + "The computation of a mesh file could not be started because no segmentation layer was found.", + ); + } + }; + + const getPrecomputeMeshesButton = () => { + const { disabled, title } = this.getPrecomputeMeshesTooltipInfo(); + return ( + + + + ); + }; + const getStlImportItem = () => ( { ); - const getLoadMeshCellButton = () => ( - - ); + const getComputeMeshAdHocButton = () => { + const { disabled, title } = this.getComputeMeshAdHocTooltipInfo(); + return ( + + + + ); + }; const loadPrecomputedMesh = async () => { const pos = getPosition(this.props.flycam); @@ -460,14 +584,17 @@ class MeshesView extends React.Component { ); }; - const getHeaderDropdownMenu = () => ( - - - {getComputeMeshfileTooltip("Compute Mesh File")} - - {getStlImportItem()} - - ); + const getHeaderDropdownMenu = () => { + const { disabled, title } = this.getPrecomputeMeshesTooltipInfo(); + return ( + + + Precompute Meshes + + {getStlImportItem()} + + ); + }; const getHeaderDropdownButton = () => ( @@ -483,85 +610,14 @@ class MeshesView extends React.Component {
{getHeaderDropdownButton()}
- {getLoadMeshCellButton()} - {this.props.currentMeshFile ? getLoadPrecomputedMeshButton() : getComputeMeshFileButton()} + {/* Only show option for ad-hoc computation if no mesh file is available */ + this.props.currentMeshFile ? null : getComputeMeshAdHocButton()} + {this.props.currentMeshFile + ? getLoadPrecomputedMeshButton() + : getPrecomputeMeshesButton()}
); - const getToggleVisibilityCheckbox = (segmentId: number, isVisible: boolean) => ( - - ) => { - this.props.onChangeVisibility(segmentId, event.target.checked); - }} - /> - - ); - - const getIsosurfaceListItem = (isosurface: IsosurfaceInformation) => { - const { segmentId, seedPosition, isLoading, isPrecomputed, isVisible } = isosurface; - const isCenteredCell = hasSegmentation - ? getSegmentIdForPosition(getPosition(this.props.flycam)) - : false; - const isHoveredItem = segmentId === this.state.hoveredListItem; - const actionVisibility = isLoading || isHoveredItem ? "visible" : "hidden"; - - const textStyle = isVisible ? {} : { fontStyle: "italic", color: "#989898" }; - return ( - { - this.setState({ hoveredListItem: segmentId }); - }} - onMouseLeave={() => { - this.setState({ hoveredListItem: null }); - }} - key={segmentId} - > -
-
- {isHoveredItem ? ( - getToggleVisibilityCheckbox(segmentId, isVisible) - ) : ( - - )}{" "} - { - this.props.changeActiveIsosurfaceId(segmentId, seedPosition, !isPrecomputed); - moveTo(seedPosition); - }} - style={textStyle} - > - Segment {segmentId} - -
-
- {getRefreshButton(segmentId, isPrecomputed, isLoading)} - {getDownloadButton(segmentId)} - {getDeleteButton(segmentId)} -
-
-
- ); - }; - - const getIsosurfaceList = () => - // $FlowIgnore[incompatible-call] flow does not know that the values passed as isosurfaces are indeed from the type IsosurfaceInformation - Object.values(this.props.isosurfaces).map(isosurface => getIsosurfaceListItem(isosurface)); return (
@@ -578,7 +634,7 @@ class MeshesView extends React.Component { }`, }} > - {getIsosurfaceList()} + {this.getIsosurfaceList()}
); diff --git a/frontend/stylesheets/trace_view/_right_menu.less b/frontend/stylesheets/trace_view/_right_menu.less index c046672bcb8..eef7e8349b9 100644 --- a/frontend/stylesheets/trace_view/_right_menu.less +++ b/frontend/stylesheets/trace_view/_right_menu.less @@ -17,7 +17,7 @@ height: 100%; } -.isosurface-list-item { +.mesh-list-item { padding-left: 5px; padding-right: 5px; border-radius: 2px;