diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 232533b0787..0f18be238e1 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -15,6 +15,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Added support for blosc compressed N5 datasets. [#7465](https://github.com/scalableminds/webknossos/pull/7465) - Added route for triggering the compute segment index worker job. [#7471](https://github.com/scalableminds/webknossos/pull/7471) - Added thumbnails to the dashboard dataset list. [#7479](https://github.com/scalableminds/webknossos/pull/7479) +- Adhoc mesh rendering is now available for ND datasets.[#7394](https://github.com/scalableminds/webknossos/pull/7394) ### Changed - Improved loading speed of the annotation list. [#7410](https://github.com/scalableminds/webknossos/pull/7410) diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts index abac7c54ca9..51fde065b8a 100644 --- a/frontend/javascripts/admin/admin_rest_api.ts +++ b/frontend/javascripts/admin/admin_rest_api.ts @@ -2173,6 +2173,7 @@ window.setMaintenance = setMaintenance; type MeshRequest = { // The position is in voxels in mag 1 position: Vector3; + additionalCoordinates: AdditionalCoordinate[] | undefined; mag: Vector3; segmentId: number; // Segment to build mesh for subsamplingStrides: Vector3; @@ -2193,6 +2194,7 @@ export function computeAdHocMesh( }> { const { position, + additionalCoordinates, cubeSize, mappingName, subsamplingStrides, @@ -2212,6 +2214,7 @@ export function computeAdHocMesh( // bounding box to calculate the mesh. This padding // is added here to the position and bbox size. position: V3.toArray(V3.sub(position, subsamplingStrides)), + additionalCoordinates: additionalCoordinates, cubeSize: V3.toArray(V3.add(cubeSize, subsamplingStrides)), // Name and type of mapping to apply before building mesh (optional) mapping: mappingName, diff --git a/frontend/javascripts/oxalis/api/api_latest.ts b/frontend/javascripts/oxalis/api/api_latest.ts index de934e03e4e..934fef2c616 100644 --- a/frontend/javascripts/oxalis/api/api_latest.ts +++ b/frontend/javascripts/oxalis/api/api_latest.ts @@ -139,6 +139,7 @@ import Constants, { AnnotationToolEnum, TDViewDisplayModeEnum, MappingStatusEnum, + EMPTY_OBJECT, } from "oxalis/constants"; import DataLayer from "oxalis/model/data_layer"; import type { OxalisModel } from "oxalis/model"; @@ -2257,8 +2258,12 @@ class DataApi { layerName, ).name; - if (Store.getState().localSegmentationData[effectiveLayerName].meshes[segmentId] != null) { + if (Store.getState().localSegmentationData[effectiveLayerName].meshes?.[segmentId] != null) { Store.dispatch(updateMeshVisibilityAction(effectiveLayerName, segmentId, isVisible)); + } else { + throw new Error( + `Mesh for segment ${segmentId} was not found in State.localSegmentationData.`, + ); } } @@ -2275,8 +2280,12 @@ class DataApi { layerName, ).name; - if (Store.getState().localSegmentationData[effectiveLayerName].meshes[segmentId] != null) { + if (Store.getState().localSegmentationData[effectiveLayerName].meshes?.[segmentId] != null) { Store.dispatch(removeMeshAction(effectiveLayerName, segmentId)); + } else { + throw new Error( + `Mesh for segment ${segmentId} was not found in State.localSegmentationData.`, + ); } } @@ -2293,7 +2302,7 @@ class DataApi { layerName, ).name; const segmentIds = Object.keys( - Store.getState().localSegmentationData[effectiveLayerName].meshes, + Store.getState().localSegmentationData[effectiveLayerName].meshes || EMPTY_OBJECT, ); for (const segmentId of segmentIds) { diff --git a/frontend/javascripts/oxalis/constants.ts b/frontend/javascripts/oxalis/constants.ts index ee602fed5de..0ad265e7820 100644 --- a/frontend/javascripts/oxalis/constants.ts +++ b/frontend/javascripts/oxalis/constants.ts @@ -381,3 +381,4 @@ export enum BLEND_MODES { export const Identity4x4 = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); export const IdentityTransform = { type: "affine", affineMatrix: Identity4x4 } as const; +export const EMPTY_OBJECT = {} as const; diff --git a/frontend/javascripts/oxalis/controller/segment_mesh_controller.ts b/frontend/javascripts/oxalis/controller/segment_mesh_controller.ts index 69ebb1c7e42..d761ebc87f2 100644 --- a/frontend/javascripts/oxalis/controller/segment_mesh_controller.ts +++ b/frontend/javascripts/oxalis/controller/segment_mesh_controller.ts @@ -9,27 +9,44 @@ import CustomLOD from "oxalis/controller/custom_lod"; import { getSegmentColorAsHSLA } from "oxalis/model/accessors/volumetracing_accessor"; import { NO_LOD_MESH_INDEX } from "oxalis/model/sagas/mesh_saga"; import Store from "oxalis/store"; +import { AdditionalCoordinate } from "types/api_flow_types"; +import { getAdditionalCoordinatesAsString } from "oxalis/model/accessors/flycam_accessor"; export default class SegmentMeshController { // meshesLODRootGroup holds lights and one group per segmentation id. // Each group can hold multiple meshes. meshesLODRootGroup: CustomLOD; - meshesGroupsPerSegmentationId: Record>> = {}; + + // meshesGroupsPerSegmentationId holds a record for every additionalCoordinatesString, then + // (nested) for each layerName, and then at the lowest level a group for each segment ID. + meshesGroupsPerSegmentationId: Record< + string, + Record>> + > = {}; constructor() { this.meshesLODRootGroup = new CustomLOD(); this.addLights(); } - hasMesh(id: number, layerName: string): boolean { - const segments = this.meshesGroupsPerSegmentationId[layerName]; - if (!segments) { - return false; - } - return segments[id] != null; + hasMesh( + id: number, + layerName: string, + additionalCoordinates?: AdditionalCoordinate[] | null, + ): boolean { + return ( + this.getMeshGroups(getAdditionalCoordinatesAsString(additionalCoordinates), layerName, id) != + null + ); } - addMeshFromVertices(vertices: Float32Array, segmentationId: number, layerName: string): void { + addMeshFromVertices( + vertices: Float32Array, + segmentationId: number, + layerName: string, + additionalCoordinates?: AdditionalCoordinate[] | undefined | null, + ): void { + if (vertices.length === 0) return; let bufferGeometry = new THREE.BufferGeometry(); bufferGeometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3)); @@ -43,6 +60,7 @@ export default class SegmentMeshController { null, NO_LOD_MESH_INDEX, layerName, + additionalCoordinates, ); } @@ -84,25 +102,29 @@ export default class SegmentMeshController { scale: Vector3 | null = null, lod: number, layerName: string, + additionalCoordinates: AdditionalCoordinate[] | null | undefined, ): void { - if (this.meshesGroupsPerSegmentationId[layerName] == null) { - this.meshesGroupsPerSegmentationId[layerName] = {}; - } - if (this.meshesGroupsPerSegmentationId[layerName][segmentationId] == null) { - this.meshesGroupsPerSegmentationId[layerName][segmentationId] = {}; - } - if (this.meshesGroupsPerSegmentationId[layerName][segmentationId][lod] == null) { - const newGroup = new THREE.Group(); - this.meshesGroupsPerSegmentationId[layerName][segmentationId][lod] = newGroup; + const additionalCoordinatesString = getAdditionalCoordinatesAsString(additionalCoordinates); + const keys = [additionalCoordinatesString, layerName, segmentationId, lod]; + const isNewlyAddedMesh = + this.meshesGroupsPerSegmentationId[additionalCoordinatesString]?.[layerName]?.[ + segmentationId + ]?.[lod] == null; + const targetGroup = _.get(this.meshesGroupsPerSegmentationId, keys, new THREE.Group()); + _.set( + this.meshesGroupsPerSegmentationId, + keys, + _.get(this.meshesGroupsPerSegmentationId, keys, targetGroup), + ); + if (isNewlyAddedMesh) { if (lod === NO_LOD_MESH_INDEX) { - this.meshesLODRootGroup.addNoLODSupportedMesh(newGroup); + this.meshesLODRootGroup.addNoLODSupportedMesh(targetGroup); } else { - this.meshesLODRootGroup.addLODMesh(newGroup, lod); + this.meshesLODRootGroup.addLODMesh(targetGroup, lod); } - // @ts-ignore - newGroup.cellId = segmentationId; + targetGroup.cellId = segmentationId; if (scale != null) { - newGroup.scale.copy(new THREE.Vector3(...scale)); + targetGroup.scale.copy(new THREE.Vector3(...scale)); } } const mesh = this.constructMesh(segmentationId, geometry); @@ -111,18 +133,17 @@ export default class SegmentMeshController { mesh.translateY(offset[1]); mesh.translateZ(offset[2]); } - - this.meshesGroupsPerSegmentationId[layerName][segmentationId][lod].add(mesh); + this.addMeshToMeshGroups(additionalCoordinatesString, layerName, segmentationId, lod, mesh); } removeMeshById(segmentationId: number, layerName: string): void { - if (this.meshesGroupsPerSegmentationId[layerName] == null) { - return; - } - if (this.meshesGroupsPerSegmentationId[layerName][segmentationId] == null) { + const additionalCoordinates = Store.getState().flycam.additionalCoordinates; + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + const meshGroups = this.getMeshGroups(additionalCoordKey, layerName, segmentationId); + if (meshGroups == null) { return; } - _.forEach(this.meshesGroupsPerSegmentationId[layerName][segmentationId], (meshGroup, lod) => { + _.forEach(meshGroups, (meshGroup, lod) => { const lodNumber = parseInt(lod); if (lodNumber !== NO_LOD_MESH_INDEX) { this.meshesLODRootGroup.removeLODMesh(meshGroup, lodNumber); @@ -130,34 +151,45 @@ export default class SegmentMeshController { this.meshesLODRootGroup.removeNoLODSupportedMesh(meshGroup); } }); - delete this.meshesGroupsPerSegmentationId[layerName][segmentationId]; + this.removeMeshFromMeshGroups(additionalCoordKey, layerName, segmentationId); } - getMeshGeometryInBestLOD(segmentId: number, layerName: string): THREE.Group { - const bestLod = Math.min( - ...Object.keys(this.meshesGroupsPerSegmentationId[layerName][segmentId]).map((lodVal) => - parseInt(lodVal), - ), - ); - return this.meshesGroupsPerSegmentationId[layerName][segmentId][bestLod]; + getMeshGeometryInBestLOD( + segmentId: number, + layerName: string, + additionalCoordinates?: AdditionalCoordinate[] | null, + ): THREE.Group | null { + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + const meshGroups = this.getMeshGroups(additionalCoordKey, layerName, segmentId); + if (meshGroups == null) return null; + const bestLod = Math.min(...Object.keys(meshGroups).map((lodVal) => parseInt(lodVal))); + return this.getMeshGroupsByLOD(additionalCoordinates, layerName, segmentId, bestLod); } - setMeshVisibility(id: number, visibility: boolean, layerName: string): void { - _.forEach(this.meshesGroupsPerSegmentationId[layerName][id], (meshGroup) => { + setMeshVisibility( + id: number, + visibility: boolean, + layerName: string, + additionalCoordinates?: AdditionalCoordinate[] | null, + ): void { + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + _.forEach(this.getMeshGroups(additionalCoordKey, layerName, id), (meshGroup) => { meshGroup.visible = visibility; }); } setMeshColor(id: number, layerName: string): void { const color = this.getColorObjectForSegment(id); - _.forEach(this.meshesGroupsPerSegmentationId[layerName][id], (meshGroup) => { - if (meshGroup) { - for (const child of meshGroup.children) { + // if in nd-dataset, set the color for all additional coordinates + for (const recordsOfLayers of Object.values(this.meshesGroupsPerSegmentationId)) { + const meshDataForOneSegment = recordsOfLayers[layerName][id]; + if (meshDataForOneSegment != null) { + for (const meshGroup of Object.values(meshDataForOneSegment)) { // @ts-ignore - child.material.color = color; + meshGroup.children.forEach((child) => (child.material.color = color)); } } - }); + } } getColorObjectForSegment(segmentId: number) { @@ -197,4 +229,44 @@ export default class SegmentMeshController { this.meshesLODRootGroup.add(directionalLight2); this.meshesLODRootGroup.add(pointLight); } + + getMeshGroupsByLOD( + additionalCoordinates: AdditionalCoordinate[] | null | undefined, + layerName: string, + segmentationId: number, + lod: number, + ): THREE.Group | null { + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + const keys = [additionalCoordKey, layerName, segmentationId, lod]; + return _.get(this.meshesGroupsPerSegmentationId, keys, null); + } + + getMeshGroups( + additionalCoordKey: string, + layerName: string, + segmentationId: number, + ): Record | null { + const keys = [additionalCoordKey, layerName, segmentationId]; + return _.get(this.meshesGroupsPerSegmentationId, keys, null); + } + + addMeshToMeshGroups( + additionalCoordKey: string, + layerName: string, + segmentationId: number, + lod: number, + mesh: THREE.Mesh, + ) { + this.meshesGroupsPerSegmentationId[additionalCoordKey][layerName][segmentationId][lod].add( + mesh, + ); + } + + removeMeshFromMeshGroups( + additionalCoordinateKey: string, + layerName: string, + segmentationId: number, + ) { + delete this.meshesGroupsPerSegmentationId[additionalCoordinateKey][layerName][segmentationId]; + } } diff --git a/frontend/javascripts/oxalis/controller/url_manager.ts b/frontend/javascripts/oxalis/controller/url_manager.ts index f80356ff1b6..f012f77ebdb 100644 --- a/frontend/javascripts/oxalis/controller/url_manager.ts +++ b/frontend/javascripts/oxalis/controller/url_manager.ts @@ -274,9 +274,12 @@ class UrlManager { for (const layerName of Object.keys(state.localSegmentationData)) { const { meshes: localMeshes, currentMeshFile } = state.localSegmentationData[layerName]; const currentMeshFileName = currentMeshFile?.meshFileName; - const meshes = Utils.values(localMeshes) - .filter(({ isVisible }) => isVisible) - .map(mapMeshInfoToUrlMeshDescriptor); + const meshes = + localMeshes != null + ? Utils.values(localMeshes as Record) + .filter(({ isVisible }) => isVisible) + .map(mapMeshInfoToUrlMeshDescriptor) + : []; if (currentMeshFileName != null || meshes.length > 0) { stateByLayer[layerName] = { diff --git a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts index faae5fb2c44..0334090cbe1 100644 --- a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.ts @@ -36,6 +36,7 @@ import { MAX_ZOOM_STEP_DIFF } from "oxalis/model/bucket_data_handling/loading_st import { getMatrixScale, rotateOnAxis } from "../reducers/flycam_reducer"; import { SmallerOrHigherInfo } from "../helpers/resolution_info"; import { getBaseVoxel } from "oxalis/model/scaleinfo"; +import { AdditionalCoordinate } from "types/api_flow_types"; export const ZOOM_STEP_INTERVAL = 1.1; @@ -272,6 +273,17 @@ function _getPosition(flycam: Flycam): Vector3 { return [matrix[12], matrix[13], matrix[14]]; } +export function getAdditionalCoordinatesAsString( + additionalCoordinates: AdditionalCoordinate[] | null | undefined, +): string { + if (additionalCoordinates != null && additionalCoordinates.length > 0) { + return additionalCoordinates + ?.map((coordinate) => `${coordinate.name}=${coordinate.value}`) + .reduce((a: string, b: string) => a.concat(b, ";"), "") as string; + } + return ""; +} + function _getFlooredPosition(flycam: Flycam): Vector3 { return map3((x) => Math.floor(x), _getPosition(flycam)); } diff --git a/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.ts b/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.ts index 7f6fcae64ed..2505c54b2d7 100644 --- a/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.ts @@ -4,6 +4,7 @@ import type { APIAnnotationInfo, APIDataset, APISegmentationLayer, + AdditionalCoordinate, AnnotationLayerDescriptor, ServerTracing, ServerVolumeTracing, @@ -34,6 +35,7 @@ import { MAX_ZOOM_STEP_DIFF } from "oxalis/model/bucket_data_handling/loading_st import { getFlooredPosition, getActiveMagIndexForLayer, + getAdditionalCoordinatesAsString, } from "oxalis/model/accessors/flycam_accessor"; import { reuseInstanceOnEquality } from "oxalis/model/accessors/accessor_helpers"; import { V3 } from "libs/mjs"; @@ -664,3 +666,35 @@ export function hasAgglomerateMapping(state: OxalisState) { return AGGLOMERATE_STATES.YES; } + +export function getMeshesForAdditionalCoordinates( + state: OxalisState, + additionalCoordinates: AdditionalCoordinate[] | null | undefined, + layerName: string, +) { + const addCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + const meshRecords = state.localSegmentationData[layerName].meshes; + if (meshRecords?.[addCoordKey] != null) { + return meshRecords[addCoordKey]; + } + return null; +} + +export function getMeshesForCurrentAdditionalCoordinates(state: OxalisState, layerName: string) { + return getMeshesForAdditionalCoordinates(state, state.flycam.additionalCoordinates, layerName); +} + +export function getMeshInfoForSegment( + state: OxalisState, + additionalCoordinates: AdditionalCoordinate[] | null, + layerName: string, + segmentId: number, +) { + const meshesForAddCoords = getMeshesForAdditionalCoordinates( + state, + additionalCoordinates, + layerName, + ); + if (meshesForAddCoords == null) return null; + return meshesForAddCoords[segmentId]; +} diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.ts b/frontend/javascripts/oxalis/model/actions/annotation_actions.ts index 7be62fc37db..077efe00d31 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.ts @@ -183,12 +183,18 @@ export const addUserBoundingBoxesAction = (userBoundingBoxes: Array +export const updateMeshVisibilityAction = ( + layerName: string, + id: number, + visibility: boolean, + additionalCoordinates?: AdditionalCoordinate[] | undefined | null, +) => ({ type: "UPDATE_MESH_VISIBILITY", layerName, id, visibility, + additionalCoordinates, } as const); export const maybeFetchMeshFilesAction = ( @@ -281,7 +287,7 @@ export const addAdHocMeshAction = ( layerName: string, segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, mappingName: string | null | undefined, mappingType: MappingType | null | undefined, ) => @@ -299,7 +305,7 @@ export const addPrecomputedMeshAction = ( layerName: string, segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, meshFileName: string, ) => ({ diff --git a/frontend/javascripts/oxalis/model/actions/flycam_actions.ts b/frontend/javascripts/oxalis/model/actions/flycam_actions.ts index 317ef7cf0a7..dacc8b1ef0d 100644 --- a/frontend/javascripts/oxalis/model/actions/flycam_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/flycam_actions.ts @@ -80,7 +80,7 @@ export const setPositionAction = (position: Vector3, dimensionToSkip?: number | dimensionToSkip, } as const); -export const setAdditionalCoordinatesAction = (values: AdditionalCoordinate[] | null) => +export const setAdditionalCoordinatesAction = (values: AdditionalCoordinate[] | null | undefined) => ({ type: "SET_ADDITIONAL_COORDINATES", values, diff --git a/frontend/javascripts/oxalis/model/actions/segmentation_actions.ts b/frontend/javascripts/oxalis/model/actions/segmentation_actions.ts index 965071c80d2..b5f0936f151 100644 --- a/frontend/javascripts/oxalis/model/actions/segmentation_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/segmentation_actions.ts @@ -16,7 +16,7 @@ export type SegmentationAction = LoadAdHocMeshAction | LoadPrecomputedMeshAction export const loadAdHocMeshAction = ( segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, extraInfo?: AdHocMeshInfo, layerName?: string, ) => @@ -32,7 +32,7 @@ export const loadAdHocMeshAction = ( export const loadPrecomputedMeshAction = ( segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, meshFileName: string, layerName?: string, ) => diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts index da21c096b74..f6ab0c2aac8 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts @@ -174,7 +174,7 @@ export const finishEditingAction = () => export const setActiveCellAction = ( segmentId: number, somePosition?: Vector3, - someAdditionalCoordinates?: AdditionalCoordinate[], + someAdditionalCoordinates?: AdditionalCoordinate[] | null, ) => ({ type: "SET_ACTIVE_CELL", @@ -186,7 +186,7 @@ export const setActiveCellAction = ( export const clickSegmentAction = ( segmentId: number, somePosition: Vector3, - someAdditionalCoordinates: AdditionalCoordinate[] | undefined, + someAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, layerName?: string, ) => ({ diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts index 2a710b98c33..869ee9ae728 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts @@ -8,7 +8,7 @@ import ThreeDMap from "libs/ThreeDMap"; import { OrthoViewWithoutTD, Vector2, Vector3, Vector4, ViewMode } from "oxalis/constants"; import constants from "oxalis/constants"; import traverse from "oxalis/model/bucket_data_handling/bucket_traversals"; -import { LoadingStrategy, PlaneRects } from "oxalis/store"; +import type { LoadingStrategy, PlaneRects } from "oxalis/store"; import { MAX_ZOOM_STEP_DIFF, getPriorityWeightForZoomStepDiff } from "../loading_strategy_logic"; // Note that the fourth component of Vector4 (if passed) is ignored, as it's not needed diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts index 86f19bb5e6f..2d6011f7e07 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts @@ -2,12 +2,15 @@ import update from "immutability-helper"; import type { Action } from "oxalis/model/actions/actions"; import type { OxalisState, UserBoundingBox, MeshInformation } from "oxalis/store"; import { V3 } from "libs/mjs"; -import { updateKey, updateKey2, updateKey4 } from "oxalis/model/helpers/deep_update"; +import { updateKey, updateKey2 } from "oxalis/model/helpers/deep_update"; import { maybeGetSomeTracing } from "oxalis/model/accessors/tracing_accessor"; import * as Utils from "libs/utils"; import { getDisplayedDataExtentInPlaneMode } from "oxalis/model/accessors/view_mode_accessor"; import { convertServerAnnotationToFrontendAnnotation } from "oxalis/model/reducers/reducer_helpers"; import _ from "lodash"; +import { getAdditionalCoordinatesAsString } from "../accessors/flycam_accessor"; +import { getMeshesForAdditionalCoordinates } from "../accessors/volumetracing_accessor"; +import { AdditionalCoordinate } from "types/api_flow_types"; const updateTracing = (state: OxalisState, shape: Partial): OxalisState => updateKey(state, "tracing", shape); @@ -47,6 +50,26 @@ const updateUserBoundingBoxes = (state: OxalisState, userBoundingBoxes: Array { + if (getMeshesForAdditionalCoordinates(state, additionalCoordinates, layerName) == null) { + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + return update(state, { + localSegmentationData: { + [layerName]: { + meshes: { + [additionalCoordKey]: { $set: [] }, + }, + }, + }, + }); + } + return state; +}; + function AnnotationReducer(state: OxalisState, action: Action): OxalisState { switch (action.type) { case "INITIALIZE_ANNOTATION": { @@ -206,62 +229,179 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { } case "UPDATE_MESH_VISIBILITY": { - const { layerName, id, visibility } = action; - const meshInfo: Partial = { - isVisible: visibility, - }; - return updateKey4(state, "localSegmentationData", layerName, "meshes", id, meshInfo); + const { layerName, id, visibility, additionalCoordinates } = action; + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + return update(state, { + localSegmentationData: { + [layerName]: { + meshes: { + [additionalCoordKey]: { + [id]: { + isVisible: { + $set: visibility, + }, + }, + }, + }, + }, + }, + }); } case "REMOVE_MESH": { const { layerName, segmentId } = action; - const { [segmentId]: _, ...remainingMeshes } = state.localSegmentationData[layerName].meshes; - return updateKey2(state, "localSegmentationData", layerName, { - meshes: remainingMeshes, + const newMeshes: Record> = {}; + const additionalCoordinates = state.flycam.additionalCoordinates; + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + const maybeMeshes = getMeshesForAdditionalCoordinates( + state, + additionalCoordinates, + layerName, + ); + if (maybeMeshes == null) { + throw Error("No mesh data found in state.localSegmentationData."); + } + if (maybeMeshes[segmentId] == null) { + return state; + } + const { [segmentId]: _, ...remainingMeshes } = maybeMeshes as Record; + newMeshes[additionalCoordKey] = remainingMeshes; + return update(state, { + localSegmentationData: { + [layerName]: { + meshes: { + $merge: newMeshes, + }, + }, + }, }); } + // Mesh information is stored in three places: the state in the store, segment_view_controller and within the mesh_saga. case "ADD_AD_HOC_MESH": { - const { layerName, segmentId, seedPosition, mappingName, mappingType } = action; + const { + layerName, + segmentId, + seedPosition, + seedAdditionalCoordinates, + mappingName, + mappingType, + } = action; const meshInfo: MeshInformation = { segmentId: segmentId, seedPosition, + seedAdditionalCoordinates, isLoading: false, isVisible: true, isPrecomputed: false, mappingName, mappingType, }; - return updateKey4(state, "localSegmentationData", layerName, "meshes", segmentId, meshInfo); + const additionalCoordinates = state.flycam.additionalCoordinates; + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + + const stateWithCurrentAddCoords = maybeAddAdditionalCoordinatesToMeshState( + state, + additionalCoordinates, + layerName, + ); + + const updatedKey = update(stateWithCurrentAddCoords, { + localSegmentationData: { + [layerName]: { + meshes: { + [additionalCoordKey]: { + [segmentId]: { + $set: meshInfo, + }, + }, + }, + }, + }, + }); + return updatedKey; } case "ADD_PRECOMPUTED_MESH": { - const { layerName, segmentId, seedPosition, meshFileName } = action; + const { layerName, segmentId, seedPosition, seedAdditionalCoordinates, meshFileName } = + action; const meshInfo: MeshInformation = { segmentId: segmentId, seedPosition, + seedAdditionalCoordinates, isLoading: false, isVisible: true, isPrecomputed: true, meshFileName, }; - return updateKey4(state, "localSegmentationData", layerName, "meshes", segmentId, meshInfo); + const additionalCoordinates = state.flycam.additionalCoordinates; + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + + const stateWithCurrentAddCoords = maybeAddAdditionalCoordinatesToMeshState( + state, + additionalCoordinates, + layerName, + ); + const updatedKey = update(stateWithCurrentAddCoords, { + localSegmentationData: { + [layerName]: { + meshes: { + [additionalCoordKey]: { + [segmentId]: { + $set: meshInfo, + }, + }, + }, + }, + }, + }); + return updatedKey; } case "STARTED_LOADING_MESH": { const { layerName, segmentId } = action; - const meshInfo: Partial = { - isLoading: true, - }; - return updateKey4(state, "localSegmentationData", layerName, "meshes", segmentId, meshInfo); + const additionalCoordKey = getAdditionalCoordinatesAsString( + state.flycam.additionalCoordinates, + ); + const updatedKey = update(state, { + localSegmentationData: { + [layerName]: { + meshes: { + [additionalCoordKey]: { + [segmentId]: { + isLoading: { + $set: true, + }, + }, + }, + }, + }, + }, + }); + return updatedKey; } case "FINISHED_LOADING_MESH": { const { layerName, segmentId } = action; - const meshInfo: Partial = { - isLoading: false, - }; - return updateKey4(state, "localSegmentationData", layerName, "meshes", segmentId, meshInfo); + const additionalCoordKey = getAdditionalCoordinatesAsString( + state.flycam.additionalCoordinates, + ); + const updatedKey = update(state, { + localSegmentationData: { + [layerName]: { + meshes: { + [additionalCoordKey]: { + [segmentId]: { + isLoading: { + $set: false, + }, + }, + }, + }, + }, + }, + }); + return updatedKey; } case "UPDATE_MESH_FILE_LIST": { diff --git a/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts b/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts index 1c4de983c8f..bc0e9944913 100644 --- a/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts @@ -40,7 +40,7 @@ import { import { updateKey2 } from "oxalis/model/helpers/deep_update"; import DiffableMap from "libs/diffable_map"; import * as Utils from "libs/utils"; -import type { ServerVolumeTracing } from "types/api_flow_types"; +import type { AdditionalCoordinate, ServerVolumeTracing } from "types/api_flow_types"; import { SetMappingAction, SetMappingEnabledAction, @@ -161,10 +161,13 @@ function handleUpdateSegment(state: OxalisState, action: UpdateSegmentAction) { const oldSegment = segments.getNullable(segmentId); let somePosition; + let someAdditionalCoordinates: AdditionalCoordinate[] | undefined | null; if (segment.somePosition) { somePosition = Utils.floor3(segment.somePosition); + someAdditionalCoordinates = segment.someAdditionalCoordinates; } else if (oldSegment != null) { somePosition = oldSegment.somePosition; + someAdditionalCoordinates = oldSegment.someAdditionalCoordinates; } else { // UPDATE_SEGMENT was called for a non-existing segment without providing // a position. This is necessary to define custom colors for segments @@ -179,7 +182,7 @@ function handleUpdateSegment(state: OxalisState, action: UpdateSegmentAction) { name: null, color: null, groupId: null, - someAdditionalCoordinates: [], + someAdditionalCoordinates: someAdditionalCoordinates, ...oldSegment, ...segment, somePosition, diff --git a/frontend/javascripts/oxalis/model/sagas/mesh_saga.ts b/frontend/javascripts/oxalis/model/sagas/mesh_saga.ts index d08b017a91c..24e7900d2fc 100644 --- a/frontend/javascripts/oxalis/model/sagas/mesh_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/mesh_saga.ts @@ -38,6 +38,7 @@ import { finishedLoadingMeshAction, startedLoadingMeshAction, TriggerMeshesDownloadAction, + updateMeshVisibilityAction, } from "oxalis/model/actions/annotation_actions"; import type { Saga } from "oxalis/model/sagas/effect-generators"; import { select } from "oxalis/model/sagas/effect-generators"; @@ -62,6 +63,7 @@ import window from "libs/window"; import { getActiveSegmentationTracing, getEditableMappingForVolumeTracingId, + getMeshInfoForSegment, getTracingForSegmentationLayer, } from "oxalis/model/accessors/volumetracing_accessor"; import { saveNowAction } from "oxalis/model/actions/save_actions"; @@ -70,10 +72,16 @@ import { getDracoLoader } from "libs/draco"; import messages from "messages"; import processTaskWithPool from "libs/async/task_pool"; import { getBaseSegmentationName } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper"; -import { RemoveSegmentAction, UpdateSegmentAction } from "../actions/volumetracing_actions"; +import { + BatchUpdateGroupsAndSegmentsAction, + RemoveSegmentAction, + UpdateSegmentAction, +} from "../actions/volumetracing_actions"; import { ResolutionInfo } from "../helpers/resolution_info"; import { type AdditionalCoordinate } from "types/api_flow_types"; import Zip from "libs/zipjs_wrapper"; +import { FlycamAction } from "../actions/flycam_actions"; +import { getAdditionalCoordinatesAsString } from "../accessors/flycam_accessor"; export const NO_LOD_MESH_INDEX = -1; const MAX_RETRY_COUNT = 5; @@ -94,9 +102,9 @@ const MESH_CHUNK_THROTTLE_LIMIT = 50; * Ad Hoc Meshes * */ -// Maps from layerName and segmentId to a ThreeDMap that stores for each chunk +// Maps from additional coordinates, layerName and segmentId to a ThreeDMap that stores for each chunk // (at x, y, z) position whether the mesh chunk was loaded. -const adhocMeshesMapByLayer: Record>> = {}; +const adhocMeshesMapByLayer: Record>>> = {}; function marchingCubeSizeInMag1(): Vector3 { return (window as any).__marchingCubeSizeInMag1 != null ? (window as any).__marchingCubeSizeInMag1 @@ -111,9 +119,17 @@ export function isMeshSTL(buffer: ArrayBuffer): boolean { return isMesh; } -function getOrAddMapForSegment(layerName: string, segmentId: number): ThreeDMap { - adhocMeshesMapByLayer[layerName] = adhocMeshesMapByLayer[layerName] || new Map(); - const meshesMap = adhocMeshesMapByLayer[layerName]; +function getOrAddMapForSegment( + layerName: string, + segmentId: number, + additionalCoordinates?: AdditionalCoordinate[] | null, +): ThreeDMap { + let additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + + const keys = [additionalCoordKey, layerName]; + // create new map if adhocMeshesMapByLayer[additionalCoordinatesString][layerName] doesn't exist yet. + _.set(adhocMeshesMapByLayer, keys, _.get(adhocMeshesMapByLayer, keys, new Map())); + const meshesMap = adhocMeshesMapByLayer[additionalCoordKey][layerName]; const maybeMap = meshesMap.get(segmentId); if (maybeMap == null) { @@ -125,12 +141,19 @@ function getOrAddMapForSegment(layerName: string, segmentId: number): ThreeDMap< return maybeMap; } -function removeMapForSegment(layerName: string, segmentId: number): void { - if (adhocMeshesMapByLayer[layerName] == null) { +function removeMapForSegment( + layerName: string, + segmentId: number, + additionalCoordinateKey: string, +): void { + if ( + adhocMeshesMapByLayer[additionalCoordinateKey] == null || + adhocMeshesMapByLayer[additionalCoordinateKey][layerName] == null + ) { return; } - adhocMeshesMapByLayer[layerName].delete(segmentId); + adhocMeshesMapByLayer[additionalCoordinateKey][layerName].delete(segmentId); } function getZoomedCubeSize(zoomStep: number, resolutionInfo: ResolutionInfo): Vector3 { @@ -223,7 +246,7 @@ function* getInfoForMeshLoading( function* loadAdHocMesh( seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, segmentId: number, removeExistingMesh: boolean = false, layerName?: string | null | undefined, @@ -236,13 +259,6 @@ function* loadAdHocMesh( return; } - if (_.size(layer.cube.additionalAxes) > 0) { - // Also see https://github.com/scalableminds/webknossos/issues/7229 - Toast.warning( - "The current segmentation layer has more than 3 dimensions. Meshes are not properly supported in this case.", - ); - } - yield* call([Model, Model.ensureSavedState]); const meshExtraInfo = yield* call(getMeshExtraInfo, layer.name, maybeExtraInfo); @@ -273,13 +289,27 @@ function* loadAdHocMesh( action.layerName === layer.name, ), }); + removeMeshWithoutVoxels(segmentId, layer.name, seedAdditionalCoordinates); +} + +function removeMeshWithoutVoxels( + segmentId: number, + layerName: string, + additionalCoordinates: AdditionalCoordinate[] | undefined | null, +) { + // If no voxels were added to the scene (e.g. because the segment doesn't have any voxels in this n-dimension), + // remove it from the store's state aswell. + const { segmentMeshController } = getSceneController(); + if (!segmentMeshController.hasMesh(segmentId, layerName, additionalCoordinates)) { + Store.dispatch(removeMeshAction(layerName, segmentId)); + } } function* loadFullAdHocMesh( layer: DataLayer, segmentId: number, position: Vector3, - additionalCoordinates: AdditionalCoordinate[] | undefined, + additionalCoordinates: AdditionalCoordinate[] | undefined | null, zoomStep: number, meshExtraInfo: AdHocMeshInfo, resolutionInfo: ResolutionInfo, @@ -321,7 +351,8 @@ function* loadFullAdHocMesh( const usePositionsFromSegmentStats = volumeTracing?.hasSegmentIndex && !volumeTracing.mappingIsEditable && - visibleSegmentationLayer?.tracingId != null; + visibleSegmentationLayer?.tracingId != null && + additionalCoordinates == null; // TODO remove in https://github.com/scalableminds/webknossos/pull/7411 let positionsToRequest = usePositionsFromSegmentStats ? yield* getChunkPositionsFromSegmentStats( tracingStoreHost, @@ -333,6 +364,11 @@ function* loadFullAdHocMesh( ) : [clippedPosition]; + if (positionsToRequest.length === 0) { + //if no positions are requested, remove the mesh, + //so that the old one isn't displayed anymore + yield* put(removeMeshAction(layer.name, segmentId)); + } while (positionsToRequest.length > 0) { const currentPosition = positionsToRequest.shift(); if (currentPosition == null) { @@ -400,7 +436,8 @@ function* maybeLoadMeshChunk( useDataStore: boolean, findNeighbors: boolean, ): Saga { - const threeDMap = getOrAddMapForSegment(layer.name, segmentId); + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); + const threeDMap = getOrAddMapForSegment(layer.name, segmentId, additionalCoordinates); if (threeDMap.get(clippedPosition)) { return []; @@ -451,6 +488,7 @@ function* maybeLoadMeshChunk( useDataStore ? dataStoreUrl : tracingStoreUrl, { position: clippedPosition, + additionalCoordinates: additionalCoordinates || undefined, mag, segmentId, subsamplingStrides, @@ -466,12 +504,17 @@ function* maybeLoadMeshChunk( segmentMeshController.removeMeshById(segmentId, layer.name); } - segmentMeshController.addMeshFromVertices(vertices, segmentId, layer.name); + segmentMeshController.addMeshFromVertices( + vertices, + segmentId, + layer.name, + additionalCoordinates, + ); return neighbors.map((neighbor) => getNeighborPosition(clippedPosition, neighbor)); } catch (exception) { retryCount++; ErrorHandling.notify(exception as Error); - console.warn("Retrying mesh generation..."); + console.warn("Retrying mesh generation due to", exception); yield* call(sleep, RETRY_WAIT_TIME * 2 ** retryCount); } } @@ -494,32 +537,50 @@ function* refreshMeshes(): Saga { // By that we avoid to remove cells that got annotated during reloading from the modifiedCells set. const currentlyModifiedCells = new Set(modifiedCells); modifiedCells.clear(); + + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); const segmentationLayer = Model.getVisibleSegmentationLayer(); if (!segmentationLayer) { return; } - adhocMeshesMapByLayer[segmentationLayer.name] = - adhocMeshesMapByLayer[segmentationLayer.name] || new Map(); - const meshesMapForLayer = adhocMeshesMapByLayer[segmentationLayer.name]; + adhocMeshesMapByLayer[additionalCoordKey][segmentationLayer.name] = + adhocMeshesMapByLayer[additionalCoordKey][segmentationLayer.name] || new Map(); + const meshesMapForLayer = adhocMeshesMapByLayer[additionalCoordKey][segmentationLayer.name]; for (const [segmentId, threeDMap] of Array.from(meshesMapForLayer.entries())) { if (!currentlyModifiedCells.has(segmentId)) { continue; } - yield* call(_refreshMeshWithMap, segmentId, threeDMap, segmentationLayer.name); + yield* call( + _refreshMeshWithMap, + segmentId, + threeDMap, + segmentationLayer.name, + additionalCoordinates, + ); } } function* refreshMesh(action: RefreshMeshAction): Saga { + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + const { segmentId, layerName } = action; - const meshInfo = yield* select( - (state) => state.localSegmentationData[layerName].meshes[segmentId], + const meshInfo = yield* select((state) => + getMeshInfoForSegment(state, additionalCoordinates, layerName, segmentId), ); + if (meshInfo == null) { + throw new Error( + `Mesh refreshing failed due to lack of mesh info for segment ${segmentId} in store.`, + ); + } + if (meshInfo.isPrecomputed) { yield* put(removeMeshAction(layerName, meshInfo.segmentId)); yield* put( @@ -532,9 +593,12 @@ function* refreshMesh(action: RefreshMeshAction): Saga { ), ); } else { - const threeDMap = adhocMeshesMapByLayer[action.layerName].get(segmentId); - if (threeDMap == null) return; - yield* call(_refreshMeshWithMap, segmentId, threeDMap, layerName); + if (adhocMeshesMapByLayer[additionalCoordKey] == null) return; + const threeDMap = adhocMeshesMapByLayer[additionalCoordKey][action.layerName].get(segmentId); + if (threeDMap == null) { + return; + } + yield* call(_refreshMeshWithMap, segmentId, threeDMap, layerName, additionalCoordinates); } } @@ -542,10 +606,16 @@ function* _refreshMeshWithMap( segmentId: number, threeDMap: ThreeDMap, layerName: string, + additionalCoordinates: AdditionalCoordinate[] | null, ): Saga { - const meshInfo = yield* select( - (state) => state.localSegmentationData[layerName].meshes[segmentId], + const meshInfo = yield* select((state) => + getMeshInfoForSegment(state, additionalCoordinates, layerName, segmentId), ); + if (meshInfo == null) { + throw new Error( + `Mesh refreshing failed due to lack of mesh info for segment ${segmentId} in store.`, + ); + } yield* call( [ErrorHandling, ErrorHandling.assert], !meshInfo.isPrecomputed, @@ -559,22 +629,18 @@ function* _refreshMeshWithMap( return; } - yield* put(startedLoadingMeshAction(layerName, segmentId)); // Remove mesh from cache. yield* call(removeMesh, removeMeshAction(layerName, segmentId), false); // The mesh should only be removed once after re-fetching the mesh first position. let shouldBeRemoved = true; - // Meshing for N-D segmentations is not yet supported. - // See https://github.com/scalableminds/webknossos/issues/7229 - const seedAdditionalCoordinates = undefined; for (const [, position] of meshPositions) { // Reload the mesh at the given position if it isn't already loaded there. // This is done to ensure that every voxel of the mesh is reloaded. yield* call( loadAdHocMesh, position, - seedAdditionalCoordinates, + additionalCoordinates, segmentId, shouldBeRemoved, layerName, @@ -585,8 +651,6 @@ function* _refreshMeshWithMap( ); shouldBeRemoved = false; } - - yield* put(finishedLoadingMeshAction(layerName, segmentId)); } /* @@ -683,7 +747,7 @@ type ChunksMap = Record { @@ -693,6 +757,7 @@ function* loadPrecomputedMeshForSegmentId( ); yield* put(startedLoadingMeshAction(layerName, id)); const dataset = yield* select((state) => state.dataset); + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); const availableMeshFiles = yield* call( dispatchMaybeFetchMeshFilesAsync, @@ -741,6 +806,7 @@ function* loadPrecomputedMeshForSegmentId( availableChunksMap, loadingOrder, scale, + additionalCoordinates, ); try { @@ -848,6 +914,7 @@ function _getLoadChunksTasks( availableChunksMap: ChunksMap, loadingOrder: number[], scale: Vector3 | null, + additionalCoordinates: AdditionalCoordinate[] | null, ) { const { segmentMeshController } = getSceneController(); const { meshFileName } = meshFile; @@ -932,6 +999,7 @@ function _getLoadChunksTasks( scale, lod, layerName, + additionalCoordinates, ); } @@ -977,6 +1045,7 @@ function _getLoadChunksTasks( null, lod, layerName, + additionalCoordinates, ); }, ); @@ -1004,7 +1073,12 @@ function sortByDistanceTo( */ function* downloadMeshCellById(cellName: string, segmentId: number, layerName: string): Saga { const { segmentMeshController } = getSceneController(); - const geometry = segmentMeshController.getMeshGeometryInBestLOD(segmentId, layerName); + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); + const geometry = segmentMeshController.getMeshGeometryInBestLOD( + segmentId, + layerName, + additionalCoordinates, + ); if (geometry == null) { const errorMessage = messages["tracing.not_mesh_available_to_download"]; @@ -1029,11 +1103,13 @@ function* downloadMeshCellsAsZIP( ): Saga { const { segmentMeshController } = getSceneController(); const zipWriter = new Zip.ZipWriter(new Zip.BlobWriter("application/zip")); + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); try { const addFileToZipWriterPromises = segments.map((element) => { const geometry = segmentMeshController.getMeshGeometryInBestLOD( element.segmentId, element.layerName, + additionalCoordinates, ); if (geometry == null) { @@ -1076,39 +1152,113 @@ function* downloadMeshCells(action: TriggerMeshesDownloadAction): Saga { } function* handleRemoveSegment(action: RemoveSegmentAction) { - // The dispatched action will make sure that the mesh entry is removed from the - // store **and** from the scene. Otherwise, the store will still contain a reference - // to the mesh even though it's not in the scene, anymore. - yield* put(removeMeshAction(action.layerName, action.segmentId)); + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + const { layerName, segmentId } = action; + if (adhocMeshesMapByLayer[additionalCoordKey]?.[layerName]?.get(segmentId) != null) { + // The dispatched action will make sure that the mesh entry is removed from the + // store **and** from the scene. Otherwise, the store will still contain a reference + // to the mesh even though it's not in the scene, anymore. + yield* put(removeMeshAction(action.layerName, action.segmentId)); + } } -function removeMesh(action: RemoveMeshAction, removeFromScene: boolean = true): void { +function* removeMesh(action: RemoveMeshAction, removeFromScene: boolean = true): Saga { + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); + const additionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); const { layerName } = action; const segmentId = action.segmentId; if (removeFromScene) { getSceneController().segmentMeshController.removeMeshById(segmentId, layerName); } - - removeMapForSegment(layerName, segmentId); + removeMapForSegment(layerName, segmentId, additionalCoordKey); } function* handleMeshVisibilityChange(action: UpdateMeshVisibilityAction): Saga { - const { id, visibility, layerName } = action; + const { id, visibility, layerName, additionalCoordinates } = action; + const { segmentMeshController } = yield* call(getSceneController); + segmentMeshController.setMeshVisibility(id, visibility, layerName, additionalCoordinates); +} + +export function* handleAdditionalCoordinateUpdate(): Saga { + // We want to prevent iterating through all additional coordinates to adjust the mesh visibility, so we store the + // previous additional coordinates in this method. Thus we have to catch SET_ADDITIONAL_COORDINATES actions in a + // while-true loop and register this saga in the root saga instead of calling from the mesh saga. + yield* take("WK_READY"); + + let previousAdditionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); const { segmentMeshController } = yield* call(getSceneController); - segmentMeshController.setMeshVisibility(id, visibility, layerName); + + while (true) { + const action = (yield* take(["SET_ADDITIONAL_COORDINATES"]) as any) as FlycamAction; + //satisfy TS + if (action.type !== "SET_ADDITIONAL_COORDINATES") { + throw new Error("Unexpected action type"); + } + const meshRecords = segmentMeshController.meshesGroupsPerSegmentationId; + + if (action.values == null || action.values.length === 0) break; + const newAdditionalCoordKey = getAdditionalCoordinatesAsString(action.values); + + for (const additionalCoordinates of [action.values, previousAdditionalCoordinates]) { + const currentAdditionalCoordKey = getAdditionalCoordinatesAsString(additionalCoordinates); + const shouldBeVisible = currentAdditionalCoordKey === newAdditionalCoordKey; + const recordsOfLayers = meshRecords[currentAdditionalCoordKey] || {}; + for (const [layerName, recordsForOneLayer] of Object.entries(recordsOfLayers)) { + const segmentIds = Object.keys(recordsForOneLayer); + for (const segmentIdAsString of segmentIds) { + const segmentId = parseInt(segmentIdAsString); + yield* put( + updateMeshVisibilityAction( + layerName, + segmentId, + shouldBeVisible, + additionalCoordinates, + ), + ); + yield* call( + { + context: segmentMeshController, + fn: segmentMeshController.setMeshVisibility, + }, + segmentId, + shouldBeVisible, + layerName, + additionalCoordinates, + ); + } + } + } + previousAdditionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); + } } function* handleSegmentColorChange(action: UpdateSegmentAction): Saga { const { segmentMeshController } = yield* call(getSceneController); + const additionalCoordinates = yield* select((state) => state.flycam.additionalCoordinates); if ( "color" in action.segment && - segmentMeshController.hasMesh(action.segmentId, action.layerName) + segmentMeshController.hasMesh(action.segmentId, action.layerName, additionalCoordinates) ) { segmentMeshController.setMeshColor(action.segmentId, action.layerName); } } +function* handleBatchSegmentColorChange( + batchAction: BatchUpdateGroupsAndSegmentsAction, +): Saga { + // Manually unpack batched actions and handle these. + // In theory, this could happen automatically. See this issue in the corresponding (rather unmaintained) package: https://github.com/tshelburne/redux-batched-actions/pull/18 + // However, there seem to be some problems with that approach (e.g., too many updates, infinite recursion) and the discussion there didn't really reach a consensus + // about the correct solution. + // This is why we stick to the manual unpacking for now. + const updateSegmentActions = batchAction.payload + .filter((action) => action.type === "UPDATE_SEGMENT") + .map((action) => call(handleSegmentColorChange, action as UpdateSegmentAction)); + yield* all(updateSegmentActions); +} + export default function* meshSaga(): Saga { // Buffer actions since they might be dispatched before WK_READY const loadAdHocMeshActionChannel = yield* actionChannel("LOAD_AD_HOC_MESH_ACTION"); @@ -1129,4 +1279,5 @@ export default function* meshSaga(): Saga { yield* takeEvery("UPDATE_MESH_VISIBILITY", handleMeshVisibilityChange); yield* takeEvery(["START_EDITING", "COPY_SEGMENTATION_LAYER"], markEditedCellAsDirty); yield* takeEvery("UPDATE_SEGMENT", handleSegmentColorChange); + yield* takeEvery("BATCH_UPDATE_GROUPS_AND_SEGMENTS", handleBatchSegmentColorChange); } diff --git a/frontend/javascripts/oxalis/model/sagas/root_saga.ts b/frontend/javascripts/oxalis/model/sagas/root_saga.ts index 849fcbf6fdd..200a885957a 100644 --- a/frontend/javascripts/oxalis/model/sagas/root_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/root_saga.ts @@ -8,7 +8,7 @@ import AnnotationSagas from "oxalis/model/sagas/annotation_saga"; import { watchDataRelevantChanges } from "oxalis/model/sagas/prefetch_saga"; import SkeletontracingSagas from "oxalis/model/sagas/skeletontracing_saga"; import ErrorHandling from "libs/error_handling"; -import meshSaga from "oxalis/model/sagas/mesh_saga"; +import meshSaga, { handleAdditionalCoordinateUpdate } from "oxalis/model/sagas/mesh_saga"; import { watchMaximumRenderableLayers, watchZ1Downsampling } from "oxalis/model/sagas/dataset_saga"; import { watchToolDeselection, watchToolReset } from "oxalis/model/sagas/annotation_tool_saga"; import SettingsSaga from "oxalis/model/sagas/settings_saga"; @@ -67,6 +67,7 @@ function* restartableSaga(): Saga { call(watchZ1Downsampling), call(warnIfEmailIsUnverified), call(listenToErrorEscalation), + call(handleAdditionalCoordinateUpdate), ]); } catch (err) { rootSagaCrashed = true; diff --git a/frontend/javascripts/oxalis/model/sagas/update_actions.ts b/frontend/javascripts/oxalis/model/sagas/update_actions.ts index 69cffebd1fb..3588e497e27 100644 --- a/frontend/javascripts/oxalis/model/sagas/update_actions.ts +++ b/frontend/javascripts/oxalis/model/sagas/update_actions.ts @@ -319,7 +319,7 @@ export function createSegmentVolumeAction( export function updateSegmentVolumeAction( id: number, anchorPosition: Vector3 | null | undefined, - additionalCoordinates: AdditionalCoordinate[] | undefined, + additionalCoordinates: AdditionalCoordinate[] | undefined | null, name: string | null | undefined, color: Vector3 | null, groupId: number | null | undefined, diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index a40f7b68792..54c6644a31f 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -226,7 +226,7 @@ export type Segment = { readonly id: number; readonly name: string | null | undefined; readonly somePosition: Vector3 | undefined; - readonly someAdditionalCoordinates: AdditionalCoordinate[] | undefined; + readonly someAdditionalCoordinates: AdditionalCoordinate[] | undefined | null; readonly creationTime: number | null | undefined; readonly color: Vector3 | null; readonly groupId: number | null | undefined; @@ -533,7 +533,7 @@ type UiInformation = { type BaseMeshInformation = { readonly segmentId: number; readonly seedPosition: Vector3; - readonly seedAdditionalCoordinates?: AdditionalCoordinate[]; + readonly seedAdditionalCoordinates?: AdditionalCoordinate[] | null; readonly isLoading: boolean; readonly isVisible: boolean; }; @@ -568,9 +568,11 @@ export type OxalisState = { readonly activeOrganization: APIOrganization | null; readonly uiInformation: UiInformation; readonly localSegmentationData: Record< - string, + string, //layerName { - readonly meshes: Record; + //for meshes, the string represents additional coordinates, number is the segment ID. + // The undefined types were added to enforce null checks when using this structure. + readonly meshes: Record | undefined> | undefined; readonly availableMeshFiles: Array | null | undefined; readonly currentMeshFile: APIMeshFile | null | undefined; // Note that for a volume tracing, this information should be stored diff --git a/frontend/javascripts/oxalis/view/action-bar/create_animation_modal.tsx b/frontend/javascripts/oxalis/view/action-bar/create_animation_modal.tsx index 5f0bfdb1624..608ff2d9567 100644 --- a/frontend/javascripts/oxalis/view/action-bar/create_animation_modal.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/create_animation_modal.tsx @@ -5,7 +5,7 @@ import React, { useState } from "react"; import { startRenderAnimationJob } from "admin/admin_rest_api"; import Toast from "libs/toast"; import _ from "lodash"; -import Store, { OxalisState, UserBoundingBox } from "oxalis/store"; +import Store, { MeshInformation, OxalisState, UserBoundingBox } from "oxalis/store"; import { getColorLayers, @@ -179,7 +179,10 @@ function CreateAnimationModal(props: Props) { if (visibleSegmentationLayer) { const availableMeshes = state.localSegmentationData[visibleSegmentationLayer.name].meshes; - meshSegmentIds = Object.values(availableMeshes) + if (availableMeshes == null) { + throw new Error("There is no mesh data in localSegmentationData."); + } + meshSegmentIds = Object.values(availableMeshes as Record) .filter((mesh) => mesh.isVisible && mesh.isPrecomputed) .map((mesh) => mesh.segmentId); diff --git a/frontend/javascripts/oxalis/view/context_menu.tsx b/frontend/javascripts/oxalis/view/context_menu.tsx index fa77fd28c93..040e5a330b5 100644 --- a/frontend/javascripts/oxalis/view/context_menu.tsx +++ b/frontend/javascripts/oxalis/view/context_menu.tsx @@ -1162,7 +1162,8 @@ function ContextMenuInner(propsWithInputRef: Props) { contextMenuPosition == null || volumeTracing == null || !hasNoFallbackLayer || - !volumeTracing.hasSegmentIndex + !volumeTracing.hasSegmentIndex || + props.additionalCoordinates != null // TODO change once statistics are available for nd-datasets ) { return []; } else { @@ -1300,7 +1301,13 @@ function ContextMenuInner(propsWithInputRef: Props) { ); - if (hasNoFallbackLayer && volumeTracing?.hasSegmentIndex && isHoveredSegmentOrMesh) { + const areSegmentStatisticsAvailable = + hasNoFallbackLayer && + volumeTracing?.hasSegmentIndex && + isHoveredSegmentOrMesh && + props.additionalCoordinates == null; // TODO change once statistics are available for nd-datasets + + if (areSegmentStatisticsAvailable) { infoRows.push( getInfoMenuItem( "volumeInfo", @@ -1314,7 +1321,7 @@ function ContextMenuInner(propsWithInputRef: Props) { ); } - if (hasNoFallbackLayer && volumeTracing?.hasSegmentIndex && isHoveredSegmentOrMesh) { + if (areSegmentStatisticsAvailable) { infoRows.push( getInfoMenuItem( "boundingBoxPositionInfo", @@ -1518,7 +1525,14 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(removeMeshAction(layerName, meshId)); }, hideMesh(layerName: string, meshId: number) { - dispatch(updateMeshVisibilityAction(layerName, meshId, false)); + dispatch( + updateMeshVisibilityAction( + layerName, + meshId, + false, + Store.getState().flycam.additionalCoordinates || undefined, + ), + ); }, setPosition(position: Vector3) { dispatch(setPositionAction(position)); diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segment_list_item.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segment_list_item.tsx index 690aae9c31c..96e32f3e6e9 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segment_list_item.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segment_list_item.tsx @@ -31,6 +31,7 @@ import { ChangeColorMenuItemContent } from "components/color_picker"; import { MenuItemType } from "antd/lib/menu/hooks/useItems"; import { withMappingActivationConfirmation } from "./segments_view_helper"; import { type AdditionalCoordinate } from "types/api_flow_types"; +import { getAdditionalCoordinatesAsString } from "oxalis/model/accessors/flycam_accessor"; function ColoredDotIconForSegment({ segmentColorHSLA }: { segmentColorHSLA: Vector4 }) { const hslaCss = hslaToCSS(segmentColorHSLA); @@ -52,7 +53,7 @@ const getLoadPrecomputedMeshMenuItem = ( loadPrecomputedMesh: ( segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, meshFileName: string, ) => void, andCloseContextMenu: (_ignore?: any) => void, @@ -111,7 +112,7 @@ const getComputeMeshAdHocMenuItem = ( loadAdHocMesh: ( segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, ) => void, isSegmentationLayerVisible: boolean, andCloseContextMenu: (_ignore?: any) => void, @@ -129,9 +130,12 @@ const getComputeMeshAdHocMenuItem = ( andCloseContextMenu(); return; } - andCloseContextMenu( - loadAdHocMesh(segment.id, segment.somePosition, segment.someAdditionalCoordinates), + loadAdHocMesh( + segment.id, + segment.somePosition, + Store.getState().flycam.additionalCoordinates, + ), ); }, disabled, @@ -144,7 +148,7 @@ const getMakeSegmentActiveMenuItem = ( setActiveCell: ( arg0: number, somePosition?: Vector3, - someAdditionalCoordinates?: AdditionalCoordinate[], + someAdditionalCoordinates?: AdditionalCoordinate[] | null, ) => void, activeCellId: number | null | undefined, isEditingDisabled: boolean, @@ -158,11 +162,7 @@ const getMakeSegmentActiveMenuItem = ( key: "setActiveCell", onClick: () => andCloseContextMenu( - setActiveCell( - segment.id, - segment.somePosition, - segment.someAdditionalCoordinates || undefined, - ), + setActiveCell(segment.id, segment.somePosition, segment.someAdditionalCoordinates), ), disabled: isActiveSegment || isEditingDisabled, label: ( @@ -197,22 +197,24 @@ type Props = { loadAdHocMesh: ( segmentId: number, somePosition: Vector3, - someAdditionalCoordinates: AdditionalCoordinate[] | undefined, + someAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, ) => void; loadPrecomputedMesh: ( segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, meshFileName: string, ) => void; setActiveCell: ( arg0: number, somePosition?: Vector3, - someAdditionalCoordinates?: AdditionalCoordinate[], + someAdditionalCoordinates?: AdditionalCoordinate[] | null, ) => void; mesh: MeshInformation | null | undefined; setPosition: (arg0: Vector3) => void; - setAdditionalCoordinates: (additionalCoordinates: AdditionalCoordinate[] | undefined) => void; + setAdditionalCoordinates: ( + additionalCoordinates: AdditionalCoordinate[] | undefined | null, + ) => void; currentMeshFile: APIMeshFile | null | undefined; onRenameStart: () => void; onRenameEnd: () => void; @@ -229,15 +231,21 @@ function _MeshInfoItem(props: { setPosition: (arg0: Vector3) => void; setAdditionalCoordinates: (additionalCoordinates: AdditionalCoordinate[] | undefined) => void; }) { + const additionalCoordinates = useSelector( + (state: OxalisState) => state.flycam.additionalCoordinates, + ); const dispatch = useDispatch(); - const onChangeMeshVisibility = (layerName: string, id: number, isVisible: boolean) => { - dispatch(updateMeshVisibilityAction(layerName, id, isVisible)); + dispatch(updateMeshVisibilityAction(layerName, id, isVisible, mesh?.seedAdditionalCoordinates)); }; const { segment, isSelectedInList, isHovered, mesh } = props; - if (!mesh) { + if ( + !mesh || + getAdditionalCoordinatesAsString(mesh.seedAdditionalCoordinates) !== + getAdditionalCoordinatesAsString(additionalCoordinates) + ) { if (isSelectedInList) { return (
{ const visibleSegmentationLayer = getVisibleSegmentationLayer(state); const activeVolumeTracing = getActiveSegmentationTracing(state); @@ -164,12 +167,14 @@ const mapStateToProps = (state: OxalisState): StateProps => { const isVisibleButUneditableSegmentationLayerActive = visibleSegmentationLayer != null && visibleSegmentationLayer.tracingId == null; + const meshesForCurrentAdditionalCoordinates = + visibleSegmentationLayer != null + ? getMeshesForCurrentAdditionalCoordinates(state, visibleSegmentationLayer?.name) + : undefined; + return { activeCellId: activeVolumeTracing?.activeCellId, - meshes: - visibleSegmentationLayer != null - ? state.localSegmentationData[visibleSegmentationLayer.name].meshes - : EMPTY_OBJECT, + meshes: meshesForCurrentAdditionalCoordinates || EMPTY_OBJECT, // satisfy ts dataset: state.dataset, isJSONMappingEnabled: mappingInfo.mappingStatus === MappingStatusEnum.ENABLED && mappingInfo.mappingType === "JSON", @@ -209,15 +214,15 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ loadAdHocMesh( segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + additionalCoordinates: AdditionalCoordinate[] | undefined | null, ) { - dispatch(loadAdHocMeshAction(segmentId, seedPosition, seedAdditionalCoordinates)); + dispatch(loadAdHocMeshAction(segmentId, seedPosition, additionalCoordinates)); }, loadPrecomputedMesh( segmentId: number, seedPosition: Vector3, - seedAdditionalCoordinates: AdditionalCoordinate[] | undefined, + seedAdditionalCoordinates: AdditionalCoordinate[] | undefined | null, meshFileName: string, ) { dispatch( @@ -228,7 +233,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ setActiveCell( segmentId: number, somePosition?: Vector3, - someAdditionalCoordinates?: AdditionalCoordinate[], + someAdditionalCoordinates?: AdditionalCoordinate[] | null, ) { dispatch(setActiveCellAction(segmentId, somePosition, someAdditionalCoordinates)); }, @@ -241,8 +246,13 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(setPositionAction(position)); }, - setAdditionalCoordinates(additionalCoordinates: AdditionalCoordinate[] | undefined) { - dispatch(setAdditionalCoordinatesAction(additionalCoordinates || null)); + setAdditionalCoordinates(additionalCoordinates: AdditionalCoordinate[] | undefined | null) { + if ( + getAdditionalCoordinatesAsString(Store.getState().flycam.additionalCoordinates) !== + getAdditionalCoordinatesAsString(additionalCoordinates) + ) { + dispatch(setAdditionalCoordinatesAction(additionalCoordinates)); + } }, updateSegments( @@ -679,8 +689,9 @@ class SegmentsView extends React.Component { return; } this.props.setPosition(segment.somePosition); - if (segment.someAdditionalCoordinates) { - this.props.setAdditionalCoordinates(segment.someAdditionalCoordinates); + const segmentAdditionalCoordinates = segment.someAdditionalCoordinates; + if (segmentAdditionalCoordinates != null) { + this.props.setAdditionalCoordinates(segmentAdditionalCoordinates); } }; @@ -1069,7 +1080,8 @@ class SegmentsView extends React.Component { visibleSegmentationLayer == null || !("fallbackLayer" in visibleSegmentationLayer) || visibleSegmentationLayer.fallbackLayer != null || - !this.props.activeVolumeTracing?.hasSegmentIndex + !this.props.activeVolumeTracing?.hasSegmentIndex || + this.props.flycam.additionalCoordinates != null // TODO change once statistics are available for nd-datasets ) { //in this case there is a fallback layer return null; @@ -1256,14 +1268,11 @@ class SegmentsView extends React.Component { } handleRefreshMeshes = (groupId: number | null) => { - const { visibleSegmentationLayer } = this.props; + const { visibleSegmentationLayer, meshes } = this.props; if (visibleSegmentationLayer == null) return; this.handlePerSegment(groupId, (segment) => { - if ( - Store.getState().localSegmentationData[visibleSegmentationLayer.name].meshes[segment.id] != - null - ) { + if (meshes[segment.id] != null) { Store.dispatch(refreshMeshAction(visibleSegmentationLayer.name, segment.id)); } }); @@ -1282,13 +1291,10 @@ class SegmentsView extends React.Component { }; handleRemoveMeshes = (groupId: number | null) => { - const { visibleSegmentationLayer } = this.props; + const { visibleSegmentationLayer, meshes } = this.props; if (visibleSegmentationLayer == null) return; this.handlePerSegment(groupId, (segment) => { - if ( - Store.getState().localSegmentationData[visibleSegmentationLayer.name].meshes[segment.id] != - null - ) { + if (meshes[segment.id] != null) { Store.dispatch(removeMeshAction(visibleSegmentationLayer.name, segment.id)); } }); @@ -1299,9 +1305,13 @@ class SegmentsView extends React.Component { groupId: number | null, isVisible: boolean, ) => { + const { flycam, meshes } = this.props; + const additionalCoordinates = flycam.additionalCoordinates; this.handlePerSegment(groupId, (segment) => { - if (Store.getState().localSegmentationData[layerName].meshes[segment.id] != null) { - Store.dispatch(updateMeshVisibilityAction(layerName, segment.id, isVisible)); + if (meshes[segment.id] != null) { + Store.dispatch( + updateMeshVisibilityAction(layerName, segment.id, isVisible, additionalCoordinates), + ); } }); }; @@ -1309,7 +1319,11 @@ class SegmentsView extends React.Component { handleLoadMeshesAdHoc = (groupId: number | null) => { this.handlePerSegment(groupId, (segment) => { if (segment.somePosition == null) return; - this.props.loadAdHocMesh(segment.id, segment.somePosition, segment.someAdditionalCoordinates); + this.props.loadAdHocMesh( + segment.id, + segment.somePosition, + this.props.flycam.additionalCoordinates, + ); }); }; @@ -1794,8 +1808,7 @@ class SegmentsView extends React.Component { const relevantSegments = groupId != null ? this.getSegmentsOfGroupRecursively(groupId) : this.getSelectedSegments(); if (relevantSegments == null || relevantSegments.length === 0) return false; - const meshesOfLayer = - Store.getState().localSegmentationData[visibleSegmentationLayer.name].meshes; + const meshesOfLayer = this.props.meshes; return relevantSegments.some((segment) => meshesOfLayer[segment.id] != null); }; diff --git a/frontend/javascripts/test/helpers/apiHelpers.ts b/frontend/javascripts/test/helpers/apiHelpers.ts index 13918692fdb..615305da6bc 100644 --- a/frontend/javascripts/test/helpers/apiHelpers.ts +++ b/frontend/javascripts/test/helpers/apiHelpers.ts @@ -11,6 +11,7 @@ import sinon from "sinon"; import window from "libs/window"; import dummyUser from "test/fixtures/dummy_user"; import dummyOrga from "test/fixtures/dummy_organization"; +import { setSceneController } from "oxalis/controller/scene_controller_provider"; import { tracing as SKELETON_TRACING, annotation as SKELETON_ANNOTATION, @@ -220,6 +221,11 @@ export function __setupOxalis( .withArgs(sinon.match((arg) => arg === `/api/users/${dummyUser.id}/taskTypeId`)) .returns(Promise.resolve(dummyUser)); + setSceneController({ + name: "This is a dummy scene controller so that getSceneController works in the tests.", + segmentMeshController: { meshesGroupsPerSegmentationId: {} }, + }); + return Model.fetch( ANNOTATION_TYPE, { diff --git a/tools/check-cyclic-dependencies.js b/tools/check-cyclic-dependencies.js index 946127e8133..b89433f47a3 100755 --- a/tools/check-cyclic-dependencies.js +++ b/tools/check-cyclic-dependencies.js @@ -1,83 +1,6 @@ const madge = require("madge"); const KNOWN_CYCLES = [ - ["types/api_flow_types.ts", "admin/organization/pricing_plan_utils.ts"], - [ - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/annotation_reducer.ts", - "oxalis/model/reducers/reducer_helpers.ts", - "oxalis/model/accessors/tool_accessor.ts", - ], - [ - "oxalis/model/accessors/volumetracing_accessor.ts", - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/annotation_reducer.ts", - "oxalis/model/reducers/reducer_helpers.ts", - "oxalis/model/accessors/tool_accessor.ts", - ], - [ - "oxalis/model/accessors/volumetracing_accessor.ts", - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/annotation_reducer.ts", - "oxalis/model/reducers/reducer_helpers.ts", - ], - [ - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/flycam_reducer.ts", - ], - [ - "oxalis/model/accessors/volumetracing_accessor.ts", - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/save_reducer.ts", - "oxalis/model/reducers/volumetracing_reducer_helpers.ts", - ], - [ - "oxalis/model/accessors/volumetracing_accessor.ts", - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/settings_reducer.ts", - ], - [ - "types/schemas/user_settings.schema.ts", - "oxalis/model/accessors/volumetracing_accessor.ts", - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/settings_reducer.ts", - ], - [ - "types/schemas/user_settings.schema.ts", - "oxalis/model/accessors/volumetracing_accessor.ts", - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/skeletontracing_reducer.ts", - ], - [ - "oxalis/model/accessors/volumetracing_accessor.ts", - "oxalis/model/accessors/flycam_accessor.ts", - "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker.ts", - "oxalis/store.ts", - "oxalis/model/reducers/volumetracing_reducer.ts", - ], - ["admin/organization/upgrade_plan_modal.tsx", "admin/organization/organization_cards.tsx"], - ["admin/team/edit_team_modal_view.tsx", "admin/team/team_list_view.tsx"], - [ - "oxalis/geometries/materials/plane_material_factory.ts", - "oxalis/shaders/main_data_shaders.glsl.ts", - "oxalis/geometries/plane.ts", - ], [ "admin/admin_rest_api.ts", "admin/api/mesh_v0.ts", @@ -86,11 +9,15 @@ const KNOWN_CYCLES = [ "admin/datastore_health_check.ts", ], ["libs/request.ts", "admin/datastore_health_check.ts"], + ["types/api_flow_types.ts", "admin/organization/pricing_plan_utils.ts"], ["libs/mjs.ts"], - ["oxalis/controller/url_manager.ts", "oxalis/model_initialization.ts"], + ["oxalis/model/accessors/flycam_accessor.ts", "oxalis/model/reducers/flycam_reducer.ts"], + ["admin/organization/upgrade_plan_modal.tsx", "admin/organization/organization_cards.tsx"], + ["admin/team/edit_team_modal_view.tsx", "admin/team/team_list_view.tsx"], [ - "oxalis/view/action-bar/tracing_actions_view.tsx", - "oxalis/view/action-bar/view_dataset_actions_view.tsx", + "oxalis/geometries/materials/plane_material_factory.ts", + "oxalis/shaders/main_data_shaders.glsl.ts", + "oxalis/geometries/plane.ts", ], ]; @@ -117,6 +44,12 @@ madge("frontend/javascripts/main.tsx", { .map((cycle) => cycle.join(" -> ")) .join("\n")}\n`, ); + } else if (cyclicDependencies.length < knownCycleStrings.length) { + throw new Error(`Congratulations! Your admirable work removed at least one cyclic dependency from the TypeScript modules. To ensure + that this improvement is not undone accidentally in the future, please adapt the KNOWN_CYCLES variable in the check-cyclic-dependies.js + script. Please set the variable to the following and commit it: + ${JSON.stringify(cyclicDependencies, null, " ")} + `); } console.log("Success."); }); diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala index bbae4da34c6..1bdc2499f4d 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/BinaryDataController.scala @@ -265,6 +265,7 @@ class BinaryDataController @Inject()( request.body.scale, request.body.mapping, request.body.mappingType, + request.body.additionalCoordinates, request.body.findNeighbors ) // The client expects the ad-hoc mesh as a flat float-array. Three consecutive floats form a 3D point, three diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala index 27c7c63e888..f84b922a462 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/DataRequests.scala @@ -54,6 +54,7 @@ case class WebknossosAdHocMeshRequest( scale: Vec3Double, mapping: Option[String] = None, mappingType: Option[String] = None, + additionalCoordinates: Option[Seq[AdditionalCoordinate]] = None, findNeighbors: Boolean = true ) { def cuboid(dataLayer: DataLayer): Cuboid = diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AdHocMeshService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AdHocMeshService.scala index 52dd69cd8e7..4d63f79bbe2 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AdHocMeshService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AdHocMeshService.scala @@ -7,6 +7,7 @@ import akka.routing.RoundRobinPool import akka.util.Timeout import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} import com.scalableminds.util.tools.{Fox, FoxImplicits} +import com.scalableminds.webknossos.datastore.models.AdditionalCoordinate import com.scalableminds.webknossos.datastore.models.datasource.{DataSource, ElementClass, SegmentationLayer} import com.scalableminds.webknossos.datastore.models.requests.{ Cuboid, @@ -31,6 +32,7 @@ case class AdHocMeshRequest(dataSource: Option[DataSource], scale: Vec3Double, mapping: Option[String] = None, mappingType: Option[String] = None, + additionalCoordinates: Option[Seq[AdditionalCoordinate]] = None, findNeighbors: Boolean = true) case class DataTypeFunctors[T, B]( @@ -172,12 +174,14 @@ class AdHocMeshService(binaryDataService: BinaryDataService, val subsamplingStrides = Vec3Double(request.subsamplingStrides.x, request.subsamplingStrides.y, request.subsamplingStrides.z) - val dataRequest = DataServiceDataRequest(request.dataSource.orNull, - request.dataLayer, - request.mapping, - cuboid, - DataServiceRequestSettings.default, - request.subsamplingStrides) + val dataRequest = DataServiceDataRequest( + request.dataSource.orNull, + request.dataLayer, + request.mapping, + cuboid, + DataServiceRequestSettings.default.copy(additionalCoordinates = request.additionalCoordinates), + request.subsamplingStrides + ) val dataDimensions = Vec3Int( math.ceil(cuboid.width / subsamplingStrides.x).toInt, diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala index 94d8eb0cdb7..1b794ac3b88 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala @@ -501,6 +501,7 @@ class VolumeTracingService @Inject()( request.scale, None, None, + request.additionalCoordinates, request.findNeighbors ) result <- adHocMeshingService.requestAdHocMeshViaActor(adHocMeshRequest)