diff --git a/frontend/javascripts/oxalis/controller/combinations/segmentation_plane_controller.js b/frontend/javascripts/oxalis/controller/combinations/segmentation_plane_controller.js index cacc1a957bb..225b0b9f369 100644 --- a/frontend/javascripts/oxalis/controller/combinations/segmentation_plane_controller.js +++ b/frontend/javascripts/oxalis/controller/combinations/segmentation_plane_controller.js @@ -93,19 +93,12 @@ export function isosurfaceLeftClick(pos: Point2, plane: OrthoView, event: MouseE let cellId = 0; const position = calculateGlobalPos(pos); - const volumeTracingMaybe = Store.getState().tracing.volume; - if (volumeTracingMaybe) { - cellId = volumeTracingMaybe.activeCellId; - } else { - const segmentation = Model.getSegmentationLayer(); - if (!segmentation) { - return; - } - cellId = segmentation.cube.getMappedDataValue( - position, - getRequestLogZoomStep(Store.getState()), - ); + const segmentation = Model.getSegmentationLayer(); + if (!segmentation) { + return; } + cellId = segmentation.cube.getMappedDataValue(position, getRequestLogZoomStep(Store.getState())); + if (cellId > 0) { Store.dispatch(changeActiveIsosurfaceCellAction(cellId, position)); } diff --git a/frontend/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.js b/frontend/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.js index 513bfd257de..0bc2d192e9d 100644 --- a/frontend/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.js +++ b/frontend/javascripts/oxalis/controller/combinations/skeletontracing_plane_controller.js @@ -49,10 +49,7 @@ import type { Edge, Tree, Node } from "oxalis/store"; import api from "oxalis/api/internal_api"; import getSceneController from "oxalis/controller/scene_controller_provider"; import { renderToTexture } from "oxalis/view/rendering_utils"; -import { - isosurfaceLeftClick, - agglomerateSkeletonMiddleClick, -} from "oxalis/controller/combinations/segmentation_plane_controller"; +import { agglomerateSkeletonMiddleClick } from "oxalis/controller/combinations/segmentation_plane_controller"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import Dimensions from "oxalis/model/dimensions"; @@ -348,8 +345,6 @@ function onClick( } else { Store.dispatch(setActiveNodeAction(nodeId)); } - } else if (shiftPressed && event != null) { - isosurfaceLeftClick(position, plane, event); } } diff --git a/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js b/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js index b79eb25604e..c2ba30a2cc5 100644 --- a/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js +++ b/frontend/javascripts/oxalis/controller/combinations/volumetracing_plane_controller.js @@ -39,7 +39,6 @@ import { movePlaneFlycamOrthoAction, setPositionAction } from "oxalis/model/acti import Model from "oxalis/model"; import Store from "oxalis/store"; import * as Utils from "libs/utils"; -import { isosurfaceLeftClick } from "oxalis/controller/combinations/segmentation_plane_controller"; // TODO: Build proper UI for this window.isAutomaticBrushEnabled = false; @@ -170,7 +169,6 @@ export function getPlaneMouseControls(_planeId: OrthoView): * { ); if (cellId > 0) { Store.dispatch(setActiveCellAction(cellId)); - isosurfaceLeftClick(pos, plane, event); } } else if (shouldFillCell) { Store.dispatch(floodFillAction(calculateGlobalPos(pos), plane)); diff --git a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js index 3289364458d..a986db8ec0b 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js +++ b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.js @@ -50,7 +50,6 @@ import getSceneController from "oxalis/controller/scene_controller_provider"; import * as skeletonController from "oxalis/controller/combinations/skeletontracing_plane_controller"; import * as volumeController from "oxalis/controller/combinations/volumetracing_plane_controller"; import { downloadScreenshot } from "oxalis/view/rendering_utils"; -import { isosurfaceLeftClick } from "oxalis/controller/combinations/segmentation_plane_controller"; const MAX_BRUSH_CHANGE_VALUE = 5; const BRUSH_CHANGING_CONSTANT = 0.02; @@ -198,12 +197,7 @@ class PlaneController extends React.PureComponent { ...skeletonControls, // $FlowIssue[exponential-spread] See https://github.com/facebook/flow/issues/8299 ...volumeControls, - leftClick: this.createToolDependentHandler( - maybeSkeletonLeftClick, - maybeVolumeLeftClick, - // The isosurfaceLeftClick handler should only be used in view mode. - isosurfaceLeftClick, - ), + leftClick: this.createToolDependentHandler(maybeSkeletonLeftClick, maybeVolumeLeftClick), leftDownMove: this.createToolDependentHandler( maybeSkeletonLeftDownMove, maybeVolumeLeftDownMove, diff --git a/frontend/javascripts/oxalis/default_state.js b/frontend/javascripts/oxalis/default_state.js index 41a0af3c5e0..c7ff97854a3 100644 --- a/frontend/javascripts/oxalis/default_state.js +++ b/frontend/javascripts/oxalis/default_state.js @@ -44,7 +44,6 @@ const defaultState: OxalisState = { loadingStrategy: "PROGRESSIVE_QUALITY", highlightHoveredCellId: true, segmentationPatternOpacity: 40, - renderIsosurfaces: false, renderMissingDataBlack: true, }, userConfiguration: { @@ -205,6 +204,7 @@ const defaultState: OxalisState = { hasOrganizations: false, isRefreshingIsosurfaces: false, }, + isosurfaces: {}, }; export default defaultState; diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index f6fafe26da2..e88a2aef0c6 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -6,6 +6,7 @@ import type { RemoteMeshMetaData, APIAnnotationVisibility, } from "types/api_flow_types"; +import type { Vector3 } from "oxalis/constants"; import type { UserBoundingBox } from "oxalis/store"; type InitializeAnnotationAction = { @@ -71,8 +72,13 @@ export type CreateMeshFromBufferAction = { name: string, }; +export type TriggerActiveIsosurfaceDownloadAction = { + type: "TRIGGER_ACTIVE_ISOSURFACE_DOWNLOAD", +}; + export type TriggerIsosurfaceDownloadAction = { type: "TRIGGER_ISOSURFACE_DOWNLOAD", + cellId: number, }; export type RefreshIsosurfacesAction = { @@ -82,6 +88,18 @@ export type RefreshIsosurfacesAction = { export type FinishedRefreshingIsosurfacesAction = { type: "FINISHED_REFRESHING_ISOSURFACES", }; +export type RefreshIsosurfaceAction = { + type: "REFRESH_ISOSURFACE", + cellId: number, +}; +export type StartRefreshingIsosurfaceAction = { + type: "START_REFRESHING_ISOSURFACE", + cellId: number, +}; +export type FinishedRefreshingIsosurfaceAction = { + type: "FINISHED_REFRESHING_ISOSURFACE", + cellId: number, +}; export type ImportIsosurfaceFromStlAction = { type: "IMPORT_ISOSURFACE_FROM_STL", @@ -93,6 +111,12 @@ export type RemoveIsosurfaceAction = { cellId: number, }; +export type AddIsosurfaceAction = { + type: "ADD_ISOSURFACE", + cellId: number, + seedPosition: Vector3, +}; + export type AnnotationActionTypes = | InitializeAnnotationAction | SetAnnotationNameAction @@ -106,11 +130,16 @@ export type AnnotationActionTypes = | DeleteMeshAction | CreateMeshFromBufferAction | UpdateLocalMeshMetaDataAction + | TriggerActiveIsosurfaceDownloadAction | TriggerIsosurfaceDownloadAction | RefreshIsosurfacesAction | FinishedRefreshingIsosurfacesAction + | RefreshIsosurfaceAction + | StartRefreshingIsosurfaceAction + | FinishedRefreshingIsosurfaceAction | ImportIsosurfaceFromStlAction - | RemoveIsosurfaceAction; + | RemoveIsosurfaceAction + | AddIsosurfaceAction; export const initializeAnnotationAction = ( annotation: APIAnnotation, @@ -198,8 +227,15 @@ export const createMeshFromBufferAction = ( name, }); -export const triggerIsosurfaceDownloadAction = (): TriggerIsosurfaceDownloadAction => ({ +export const triggerActiveIsosurfaceDownloadAction = (): TriggerActiveIsosurfaceDownloadAction => ({ + type: "TRIGGER_ACTIVE_ISOSURFACE_DOWNLOAD", +}); + +export const triggerIsosurfaceDownloadAction = ( + cellId: number, +): TriggerIsosurfaceDownloadAction => ({ type: "TRIGGER_ISOSURFACE_DOWNLOAD", + cellId, }); export const refreshIsosurfacesAction = (): RefreshIsosurfacesAction => ({ @@ -210,6 +246,25 @@ export const finishedRefreshingIsosurfacesAction = (): FinishedRefreshingIsosurf type: "FINISHED_REFRESHING_ISOSURFACES", }); +export const refreshIsosurfaceAction = (cellId: number): RefreshIsosurfaceAction => ({ + type: "REFRESH_ISOSURFACE", + cellId, +}); + +export const startRefreshingIsosurfaceAction = ( + cellId: number, +): StartRefreshingIsosurfaceAction => ({ + type: "START_REFRESHING_ISOSURFACE", + cellId, +}); + +export const finishedRefreshingIsosurfaceAction = ( + cellId: number, +): FinishedRefreshingIsosurfaceAction => ({ + type: "FINISHED_REFRESHING_ISOSURFACE", + cellId, +}); + export const importIsosurfaceFromStlAction = ( buffer: ArrayBuffer, ): ImportIsosurfaceFromStlAction => ({ @@ -221,3 +276,12 @@ export const removeIsosurfaceAction = (cellId: number): RemoveIsosurfaceAction = type: "REMOVE_ISOSURFACE", cellId, }); + +export const addIsosurfaceAction = ( + cellId: number, + seedPosition: Vector3, +): AddIsosurfaceAction => ({ + type: "ADD_ISOSURFACE", + cellId, + seedPosition, +}); diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js index 08a807a03a3..9472b1ca4fc 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -107,6 +107,35 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { return updateKey(state, "tracing", { meshes: newMeshes }); } + case "REMOVE_ISOSURFACE": { + const { cellId } = action; + return update(state, { + isosurfaces: { $unset: [cellId] }, + }); + } + + case "ADD_ISOSURFACE": { + const { cellId, seedPosition } = action; + return updateKey2(state, "isosurfaces", cellId.toString(), { + segmentId: cellId, + seedPosition, + }); + } + + case "START_REFRESHING_ISOSURFACE": { + const { cellId } = action; + return updateKey2(state, "isosurfaces", cellId.toString(), { + isLoading: true, + }); + } + + case "FINISHED_REFRESHING_ISOSURFACE": { + const { cellId } = action; + return updateKey2(state, "isosurfaces", cellId.toString(), { + isLoading: false, + }); + } + default: return state; } diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index 2cc77b50393..1cd1846d47b 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -11,9 +11,14 @@ import { type Vector3 } from "oxalis/constants"; import { type FlycamAction, FlycamActions } from "oxalis/model/actions/flycam_actions"; import { removeIsosurfaceAction, + addIsosurfaceAction, finishedRefreshingIsosurfacesAction, + startRefreshingIsosurfaceAction, + finishedRefreshingIsosurfaceAction, type ImportIsosurfaceFromStlAction, type RemoveIsosurfaceAction, + type RefreshIsosurfaceAction, + type TriggerIsosurfaceDownloadAction, } from "oxalis/model/actions/annotation_actions"; import { type Saga, @@ -111,7 +116,8 @@ function getNeighborPosition( // (active cell id is only defined in volume annotations, mapping support // for datasets is limited in volume tracings etc.), we use another state // variable for the "active cell" in view mode. The cell can be changed via -// shift+click (similar to the volume tracing mode). +// shift + click on the isosurface, by clicking on its list entry in the +// meshes tab or by clicking on the "load isosurface for centered cell" button. let currentViewIsosurfaceCellId = 0; // The calculation of an isosurface is spread across multiple requests. // In order to avoid, that too many chunks are computed for one user interaction, @@ -121,7 +127,6 @@ const MAXIMUM_BATCH_SIZE = 50; function* changeActiveIsosurfaceCell(action: ChangeActiveIsosurfaceCellAction): Saga { currentViewIsosurfaceCellId = action.cellId; - yield* call(ensureSuitableIsosurface, null, action.seedPosition, currentViewIsosurfaceCellId); } @@ -146,23 +151,34 @@ function* ensureSuitableIsosurface( if (segmentId === 0) { return; } - const renderIsosurfaces = yield* select(state => state.datasetConfiguration.renderIsosurfaces); - if (!renderIsosurfaces) { - return; - } + yield* call(loadIsosurfaceForSegmentId, segmentId, seedPosition, removeExistingIsosurface); +} + +function* getInfoForIsosurfaceLoading(): Saga<{ + dataset: APIDataset, + layer: ?DataLayer, + zoomStep: number, + resolutionInfo: ResolutionInfo, +}> { const dataset = yield* select(state => state.dataset); const layer = Model.getSegmentationLayer(); - if (!layer) { - return; - } - const position = - seedPosition != null ? seedPosition : yield* select(state => getFlooredPosition(state.flycam)); const resolutionInfo = getResolutionInfo(layer.resolutions); const preferredZoomStep = window.__isosurfaceZoomStep != null ? window.__isosurfaceZoomStep : 1; const zoomStep = resolutionInfo.getClosestExistingIndex(preferredZoomStep); + return { dataset, layer, zoomStep, resolutionInfo }; +} - const clippedPosition = clipPositionToCubeBoundary(position, zoomStep, resolutionInfo); +function* loadIsosurfaceForSegmentId( + segmentId: number, + seedPosition: ?Vector3, + removeExistingIsosurface: boolean = false, +): Saga { + const { dataset, layer, zoomStep, resolutionInfo } = yield* call(getInfoForIsosurfaceLoading); + + if (!layer) { + return; + } batchCounterPerSegment[segmentId] = 0; yield* call( @@ -170,7 +186,7 @@ function* ensureSuitableIsosurface( dataset, layer, segmentId, - clippedPosition, + seedPosition, zoomStep, resolutionInfo, removeExistingIsosurface, @@ -181,21 +197,27 @@ function* loadIsosurfaceWithNeighbors( dataset: APIDataset, layer: DataLayer, segmentId: number, - clippedPosition: Vector3, + seedPosition: ?Vector3, zoomStep: number, resolutionInfo: ResolutionInfo, removeExistingIsosurface: boolean, ): Saga { let isInitialRequest = true; + const position = + seedPosition != null ? seedPosition : yield* select(state => getFlooredPosition(state.flycam)); + const clippedPosition = clipPositionToCubeBoundary(position, zoomStep, resolutionInfo); let positionsToRequest = [clippedPosition]; + if (seedPosition) { + yield* put(addIsosurfaceAction(segmentId, seedPosition)); + } while (positionsToRequest.length > 0) { - const position = positionsToRequest.shift(); + const currentPosition = positionsToRequest.shift(); const neighbors = yield* call( maybeLoadIsosurface, dataset, layer, segmentId, - position, + currentPosition, zoomStep, resolutionInfo, removeExistingIsosurface && isInitialRequest, @@ -275,10 +297,9 @@ function* maybeLoadIsosurface( ); } -function* downloadActiveIsosurfaceCell(): Saga { - const currentId = yield* call(getCurrentCellId); +function* downloadIsosurfaceCellById(cellId: number): Saga { const sceneController = getSceneController(); - const geometry = sceneController.getIsosurfaceGeometry(currentId); + 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]); @@ -291,10 +312,19 @@ function* downloadActiveIsosurfaceCell(): Saga { isosurfaceMarker.forEach((marker, index) => { stl.setUint8(index, marker); }); - stl.setUint32(cellIdIndex, currentId, true); + stl.setUint32(cellIdIndex, cellId, true); const blob = new Blob([stl]); - yield* call(saveAs, blob, `isosurface-${currentId}.stl`); + yield* call(saveAs, blob, `isosurface-${cellId}.stl`); +} + +function* downloadIsosurfaceCell(action: TriggerIsosurfaceDownloadAction): Saga { + yield* call(downloadIsosurfaceCellById, action.cellId); +} + +function* downloadActiveIsosurfaceCell(): Saga { + const currentId = yield* call(getCurrentCellId); + yield* call(downloadIsosurfaceCellById, currentId); } function* importIsosurfaceFromStl(action: ImportIsosurfaceFromStlAction): Saga { @@ -304,6 +334,7 @@ function* importIsosurfaceFromStl(action: ImportIsosurfaceFromStlAction): Saga { } function* refreshIsosurfaces(): Saga { - const renderIsosurfaces = yield* select(state => state.datasetConfiguration.renderIsosurfaces); - if (!renderIsosurfaces) { - return; - } yield* put(saveNowAction()); // We reload all cells that got modified till the start of reloading. // By that we avoid that removing cells that got annotated during reloading from the modifiedCells set. @@ -377,13 +404,39 @@ function* refreshIsosurfaces(): Saga { yield* put(finishedRefreshingIsosurfacesAction()); } +function* refreshIsosurface(action: RefreshIsosurfaceAction): Saga { + const { cellId } = action; + + const threeDMap = isosurfacesMap.get(cellId); + const isosurfacePositions = threeDMap + ? threeDMap.entries().filter(([value, _position]) => value) + : []; + if (isosurfacePositions.length === 0) { + return; + } + yield* put(startRefreshingIsosurfaceAction(cellId)); + // Removing Isosurface from cache. + yield* call(removeIsosurface, removeIsosurfaceAction(cellId), false); + // The isosurface should only be removed once after re-fetching the isosurface first position. + let shouldBeRemoved = true; + for (const [, position] of isosurfacePositions) { + // Reload the Isosurface at the given position if it isn't already loaded there. + // This is done to ensure that every voxel of the isosurface is reloaded. + yield* call(ensureSuitableIsosurface, null, position, cellId, shouldBeRemoved); + shouldBeRemoved = false; + } + yield* put(finishedRefreshingIsosurfaceAction(cellId)); +} + export default function* isosurfaceSaga(): Saga { yield* take("WK_READY"); yield _takeEvery(FlycamActions, ensureSuitableIsosurface); yield _takeEvery("CHANGE_ACTIVE_ISOSURFACE_CELL", changeActiveIsosurfaceCell); - yield _takeEvery("TRIGGER_ISOSURFACE_DOWNLOAD", downloadActiveIsosurfaceCell); + yield _takeEvery("TRIGGER_ACTIVE_ISOSURFACE_DOWNLOAD", downloadActiveIsosurfaceCell); + yield _takeEvery("TRIGGER_ISOSURFACE_DOWNLOAD", downloadIsosurfaceCell); yield _takeEvery("IMPORT_ISOSURFACE_FROM_STL", importIsosurfaceFromStl); yield _takeEvery("REMOVE_ISOSURFACE", removeIsosurface); yield _takeEvery("REFRESH_ISOSURFACES", refreshIsosurfaces); + yield _takeEvery("REFRESH_ISOSURFACE", refreshIsosurface); yield _takeEvery(["START_EDITING", "COPY_SEGMENTATION_LAYER"], markEditedCellAsDirty); } diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index a346a70064e..08a17bbf4f4 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -275,7 +275,6 @@ export type DatasetConfiguration = {| [name: string]: DatasetLayerConfiguration, }, +highlightHoveredCellId: boolean, - +renderIsosurfaces: boolean, +position?: Vector3, +zoom?: number, +rotation?: Vector3, @@ -454,6 +453,12 @@ type UiInformation = { +isRefreshingIsosurfaces: boolean, }; +type IsosurfaceInformation = { + +segmentId: number, + +seedPosition: Vector3, + +isLoading: boolean, +}; + export type OxalisState = {| +datasetConfiguration: DatasetConfiguration, +userConfiguration: UserConfiguration, @@ -466,6 +471,7 @@ export type OxalisState = {| +viewModeData: ViewModeData, +activeUser: ?APIUser, +uiInformation: UiInformation, + +isosurfaces: { [segmentId: string]: IsosurfaceInformation }, |}; const sagaMiddleware = createSagaMiddleware(); diff --git a/frontend/javascripts/oxalis/view/right-menu/meshes_view.js b/frontend/javascripts/oxalis/view/right-menu/meshes_view.js index aa7a33bb7a9..d91f2de2619 100644 --- a/frontend/javascripts/oxalis/view/right-menu/meshes_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/meshes_view.js @@ -1,30 +1,39 @@ // @flow -import { Button, Checkbox, Icon, Input, Modal, Spin, Tooltip, Upload } from "antd"; +import { Button, Checkbox, Icon, Input, List, Modal, Spin, Tooltip, Upload } from "antd"; import type { Dispatch } from "redux"; import { connect } from "react-redux"; import React from "react"; +import _ from "lodash"; import type { ExtractReturn } from "libs/type_helpers"; import type { MeshMetaData, RemoteMeshMetaData } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; +import Store from "oxalis/store"; +import Model from "oxalis/model"; import type { Vector3 } from "oxalis/constants"; import { Vector3Input } from "libs/vector_input"; import { createMeshFromBufferAction, deleteMeshAction, importIsosurfaceFromStlAction, + triggerActiveIsosurfaceDownloadAction, triggerIsosurfaceDownloadAction, updateLocalMeshMetaDataAction, updateRemoteMeshMetaDataAction, + removeIsosurfaceAction, + refreshIsosurfaceAction, } from "oxalis/model/actions/annotation_actions"; +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, getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; +import { getSegmentationLayer } from "oxalis/model/accessors/dataset_accessor"; import { isIsosurfaceStl } from "oxalis/model/sagas/isosurface_saga"; import { readFileAsArrayBuffer } from "libs/read_file"; import { setImportingMeshStateAction } from "oxalis/model/actions/ui_actions"; -import ButtonComponent from "oxalis/view/components/button_component"; import { trackAction } from "oxalis/model/helpers/analytics"; - -const ButtonGroup = Button.Group; +import { jsConvertCellIdToHSLA } from "oxalis/shaders/segmentation.glsl"; export const stlIsosurfaceConstants = { isosurfaceMarker: [105, 115, 111], // ASCII codes for ISO @@ -75,13 +84,22 @@ class EditMeshModal extends React.PureComponent< const mapStateToProps = (state: OxalisState) => ({ meshes: state.tracing != null ? state.tracing.meshes : [], isImporting: state.uiInformation.isImportingMesh, - isHybrid: state.tracing.volume != null && state.tracing.skeleton != null, + isosurfaces: state.isosurfaces, + datasetConfiguration: state.datasetConfiguration, + mappingColors: state.temporaryConfiguration.activeMapping.mappingColors, + flycam: state.flycam, + activeCellId: state.tracing.volume ? state.tracing.volume.activeCellId : null, + segmentationLayer: getSegmentationLayer(state.dataset), + zoomStep: getRequestLogZoomStep(state), }); 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)); }, @@ -101,7 +119,11 @@ const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ } }, downloadIsosurface() { - dispatch(triggerIsosurfaceDownloadAction()); + dispatch(triggerActiveIsosurfaceDownloadAction()); + }, + changeActiveIsosurfaceId(cellId: ?number, seedPosition: Vector3) { + if (cellId == null) return; + dispatch(changeActiveIsosurfaceCellAction(cellId, seedPosition)); }, }); @@ -112,7 +134,6 @@ type OwnProps = {| type StateProps = {| meshes: Array, isImporting: boolean, - isHybrid: true, |}; type DispatchProps = ExtractReturn; @@ -126,39 +147,217 @@ const getCheckboxStyle = isLoaded => color: "#989898", }; -class MeshesView extends React.Component { +class MeshesView extends React.Component< + Props, + { currentlyEditedMesh: ?MeshMetaData, hoveredListItem: ?number }, +> { state = { currentlyEditedMesh: null, + hoveredListItem: null, }; render() { - return ( -
- - false} - onChange={file => { - this.props.onStlUpload(file); - }} - showUploadList={false} + const getSegmentationCube = () => { + const layer = Model.getSegmentationLayer(); + if (!layer) { + throw new Error("No segmentation layer found"); + } + return layer.cube; + }; + const getIdForPos = pos => getSegmentationCube().getDataValue(pos, null, this.props.zoomStep); + + const moveTo = (seedPosition: Vector3) => { + Store.dispatch(setPositionAction(seedPosition)); + }; + + const getDownloadButton = (segmentId: number) => ( + + Store.dispatch(triggerIsosurfaceDownloadAction(segmentId))} + /> + + ); + const getRefreshButton = (segmentId: number, isLoading: boolean) => ( + + { + Store.dispatch(refreshIsosurfaceAction(segmentId)); + }} + /> + + ); + const getDeleteButton = (segmentId: number) => ( + + { + // does not work properly for imported isosurfaces + Store.dispatch(removeIsosurfaceAction(segmentId)); + // reset the active isosurface id so the deleted one is not reloaded immediately + this.props.changeActiveIsosurfaceId(0, [0, 0, 0]); + }} + /> + + ); + const convertHSLAToCSSString = ([h, s, l, a]) => + `hsla(${360 * h}, ${100 * s}%, ${100 * l}%, ${a})`; + const convertCellIdToCSS = id => + convertHSLAToCSSString(jsConvertCellIdToHSLA(id, this.props.mappingColors)); + + const getImportButton = () => ( + + false} + onChange={file => { + this.props.onStlUpload(file); + }} + showUploadList={false} + style={{ fontSize: 16, color: "#2a3a48", cursor: "pointer" }} + disabled={this.props.isImporting} + > + - + + + + ); + const getLoadIsosurfaceCellButton = () => ( + + ); + const getIsosurfacesHeader = () => ( + + Isosurfaces{" "} + + + + {getImportButton()} + {getLoadIsosurfaceCellButton()} +
+
+ ); + const getMeshesHeader = () => ( +
+ Meshes{" "} + + + + {getImportButton()} +
+ ); + + const renderIsosurfaceListItem = (isosurface: Object) => { + const { segmentId, seedPosition, isLoading } = isosurface; + const centeredCell = getIdForPos(getPosition(this.props.flycam)); + const actionVisibility = segmentId === this.state.hoveredListItem ? "visible" : "hidden"; + return ( + { + this.setState({ hoveredListItem: segmentId }); + }} + onMouseLeave={() => { + this.setState({ hoveredListItem: null }); + }} + > +
+
{ + this.props.changeActiveIsosurfaceId(segmentId); + moveTo(seedPosition); + }} > - Import - - - {this.props.isHybrid ? null : ( - - - Download - - - )} - + {" "} + Segment {segmentId} +
+
+ {getDownloadButton(segmentId)} + {getRefreshButton(segmentId, isLoading)} + {getDeleteButton(segmentId)} +
+
+
+ ); + }; + const renderMeshListItem = (mesh: Object) => { + const isLoading = mesh.isLoading === true; + return ( +
+ ) => { + this.props.onChangeVisibility(mesh, event.target.checked); + }} + disabled={isLoading} + style={getCheckboxStyle(mesh.isLoaded)} + > + {mesh.description} + + {mesh.isLoaded ? ( + + this.setState({ currentlyEditedMesh: mesh })} + style={{ cursor: "pointer" }} + /> + this.props.deleteMesh(mesh.id)} + style={{ cursor: "pointer" }} + /> + + ) : null} + +
+ ); + }; + + return ( +
+ {getIsosurfacesHeader()} + {this.state.currentlyEditedMesh != null ? ( this.setState({ currentlyEditedMesh: null })} /> ) : null} - - {this.props.meshes.map(mesh => { - // Coerce nullable isLoading to a proper boolean - const isLoading = mesh.isLoading === true; - return ( -
- ) => { - this.props.onChangeVisibility(mesh, event.target.checked); - }} - disabled={isLoading} - style={getCheckboxStyle(mesh.isLoaded)} - > - {mesh.description} - - {mesh.isLoaded ? ( - - this.setState({ currentlyEditedMesh: mesh })} - style={{ cursor: "pointer" }} - /> - this.props.deleteMesh(mesh.id)} - style={{ cursor: "pointer" }} - /> - - ) : null} - -
- ); - })} + {getMeshesHeader()} +
); } diff --git a/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js b/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js index 7adefec28c0..d658b87aa00 100644 --- a/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js +++ b/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js @@ -476,13 +476,6 @@ class DatasetSettings extends React.PureComponent { onChange={_.partial(this.props.onChange, "highlightHoveredCellId")} /> )} - {!isColorLayer ? ( - - ) : null}
)} diff --git a/frontend/javascripts/oxalis/view/td_view_controls.js b/frontend/javascripts/oxalis/view/td_view_controls.js index 5c1ae8c8faf..3c146da06f4 100644 --- a/frontend/javascripts/oxalis/view/td_view_controls.js +++ b/frontend/javascripts/oxalis/view/td_view_controls.js @@ -9,12 +9,11 @@ import api from "oxalis/api/internal_api"; const ButtonGroup = Button.Group; type Props = {| - renderIsosurfaces: boolean, isRefreshingIsosurfaces: boolean, volumeTracing: ?VolumeTracing, |}; -function TDViewControls({ renderIsosurfaces, isRefreshingIsosurfaces, volumeTracing }: Props) { +function TDViewControls({ isRefreshingIsosurfaces, volumeTracing }: Props) { let refreshIsosurfaceTooltip = "Load Isosurface of centered cell from segmentation layer."; if (volumeTracing != null) { if (volumeTracing.fallbackLayer != null) { @@ -40,23 +39,20 @@ function TDViewControls({ renderIsosurfaces, isRefreshingIsosurfaces, volumeTrac XZ - {renderIsosurfaces ? ( - -