Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add loaded meshes to sharing link #5993

Merged
merged 39 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a95d9d4
add Utils.values which is Object.values but with the correct type, cl…
daniel-wer Jan 26, 2022
0a65443
allow to share ad-hoc computed meshes via link
daniel-wer Jan 26, 2022
6025ca3
clean up isosurface related code, remove auto-loading triggered by fl…
daniel-wer Jan 27, 2022
c56a556
refactor loadPrecomputedMesh method into a saga and adapt usages
daniel-wer Jan 27, 2022
f486a46
floor mesh seed position in url
daniel-wer Jan 27, 2022
8553d24
log to airbrake if the ad-hoc mesh loading limit is reached
daniel-wer Jan 27, 2022
a4f3602
load precomputed meshes from the inside out by sorting chunks
daniel-wer Jan 27, 2022
99fe876
remove unnecessary types
daniel-wer Jan 31, 2022
e39f12d
PR feedback 1
daniel-wer Jan 31, 2022
16d493b
rework task_pool to be a saga and to execute sagas to make it fully c…
daniel-wer Jan 31, 2022
dc7f772
fix mesh sharing via link if no mesh file is active
daniel-wer Feb 1, 2022
8acfa37
more PR feedback
daniel-wer Feb 1, 2022
350f734
include additional information in url for meshes to make sure the sam…
daniel-wer Feb 3, 2022
88abd54
use new kubernetix url
philippotto Feb 8, 2022
e051c30
trigger nightly (needs to be reverted afterwards)
philippotto Feb 8, 2022
c8b8c6b
Merge branch 'master' of github.com:scalableminds/webknossos into mes…
daniel-wer Feb 9, 2022
26a9aa8
fix refresh-datasets in nightly and other outdated kubernetix link
philippotto Feb 9, 2022
d105dbf
upgrade puppeteer from 1.13 to 13.2
philippotto Feb 9, 2022
01a895e
use mappingInfo parameter instead of mappingName and mappingType, PR …
daniel-wer Feb 9, 2022
25adf35
fix deprecation warning
philippotto Feb 9, 2022
498dddd
update two snapshots which only changed subtly
philippotto Feb 9, 2022
13bea43
fix url json schema and fix that mappingInfo was overwritten by meshInfo
daniel-wer Feb 9, 2022
c114ba4
update changelog
daniel-wer Feb 9, 2022
2a98dfd
Downgrade puppeteer to 11.0.0 to avoid that some segments are rendere…
daniel-wer Feb 10, 2022
961f8a1
adapt screenshot snapshots to sensible scalebar
philippotto Feb 14, 2022
b382b1b
trigger nightly now
philippotto Feb 14, 2022
779f669
Merge branch 'master' of github.com:scalableminds/webknossos into mes…
daniel-wer Feb 17, 2022
7441470
add screenshot test for mesh linking, add more logging to screenshot …
daniel-wer Feb 17, 2022
2139c83
adapt docs for sharing link format and hide the format behind a toggl…
daniel-wer Feb 17, 2022
968ad00
use large resource class for nightly tests as well
daniel-wer Feb 17, 2022
9f8c2b3
change screenshot test url temporarily and add more debugging output
daniel-wer Feb 17, 2022
1369467
Merge branch 'meshes-in-link' of github.com:scalableminds/webknossos …
daniel-wer Feb 17, 2022
9d2bdf1
Merge branch 'adapt-screenshots-to-scalebar' of github.com:scalablemi…
daniel-wer Feb 17, 2022
82a1aa2
update puppeteer and pixelmatch, --use-gl=egl, update screenshots, ma…
daniel-wer Feb 17, 2022
b2a385f
check chrome version in CI
daniel-wer Feb 18, 2022
3f779fd
use switftshader and replace screenshots, temporarily slim down night…
daniel-wer Feb 18, 2022
5598681
test meshesinlink PR
daniel-wer Feb 18, 2022
7325ff8
replace meshes screenshot with the one from the CI
daniel-wer Feb 18, 2022
5d47edc
revert temporary changes
daniel-wer Feb 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions frontend/javascripts/admin/admin_rest_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ import {
type WkConnectDatasetConfig,
type APIMeshFile,
} from "types/api_flow_types";
import { ControlModeEnum, type Vector3, type Vector6, MappingStatusEnum } from "oxalis/constants";
import { ControlModeEnum, type Vector3, type Vector6 } from "oxalis/constants";
import type {
DatasetConfiguration,
Tracing,
TraceOrViewCommand,
AnnotationType,
ActiveMappingInfo,
MappingType,
VolumeTracing,
} from "oxalis/store";
import type { NewTask, TaskCreationResponseContainer } from "admin/task/task_create_bulk_view";
Expand Down Expand Up @@ -1734,25 +1734,31 @@ export function getMeshData(id: string): Promise<ArrayBuffer> {

// These parameters are bundled into an object to avoid that the computeIsosurface function
// receives too many parameters, since this doesn't play well with the saga typings.
type IsosurfaceRequest = {
type IsosurfaceRequest = {|
position: Vector3,
zoomStep: number,
segmentId: number,
voxelDimensions: Vector3,
cubeSize: Vector3,
scale: Vector3,
};
mappingName: ?string,
mappingType: ?MappingType,
|};

export function computeIsosurface(
requestUrl: string,
mappingInfo: ActiveMappingInfo,
isosurfaceRequest: IsosurfaceRequest,
): Promise<{ buffer: ArrayBuffer, neighbors: Array<number> }> {
const { position, zoomStep, segmentId, voxelDimensions, cubeSize, scale } = isosurfaceRequest;
const mapping =
mappingInfo.mappingStatus !== MappingStatusEnum.DISABLED ? mappingInfo.mappingName : undefined;
const mappingType =
mappingInfo.mappingStatus !== MappingStatusEnum.DISABLED ? mappingInfo.mappingType : undefined;
const {
position,
zoomStep,
segmentId,
voxelDimensions,
cubeSize,
scale,
mappingName,
mappingType,
} = isosurfaceRequest;
return doWithToken(async token => {
const { buffer, headers } = await Request.sendJSONReceiveArraybufferWithHeaders(
`${requestUrl}/isosurface?token=${token}`,
Expand All @@ -1766,8 +1772,8 @@ export function computeIsosurface(
zoomStep,
// Segment to build mesh for
segmentId,
// Name of mapping to apply before building mesh (optional)
mapping,
// Name and type of mapping to apply before building mesh (optional)
mapping: mappingName,
mappingType,
// "size" of each voxel (i.e., only every nth voxel is considered in each dimension)
voxelDimensions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import update from "immutability-helper";
import type { APIUser, APITeam, APITeamMembership } from "types/api_flow_types";
import { updateUser, getEditableTeams } from "admin/admin_rest_api";
import messages from "messages";
import * as Utils from "libs/utils";

const RadioButton = Radio.Button;
const RadioGroup = Radio.Group;
Expand Down Expand Up @@ -129,7 +130,7 @@ class PermissionsAndTeamsModalView extends React.PureComponent<TeamRoleModalProp
setPermissionsAndTeams = () => {
const newUserPromises = this.props.users.map(user => {
if (this.props.selectedUserIds.includes(user.id)) {
const newTeams = ((Object.values(this.state.selectedTeams): any): Array<APITeamMembership>);
const newTeams = Utils.values(this.state.selectedTeams);
const newUser = Object.assign({}, user, { teams: newTeams });
if (this.props.activeUser.isAdmin && this.props.selectedUserIds.length === 1) {
// If the current user is admin and only one user is edited we also update the permissions.
Expand Down
67 changes: 41 additions & 26 deletions frontend/javascripts/libs/task_pool.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,51 @@
// @flow
import { type Saga, type Task, join, call, fork } from "oxalis/model/sagas/effect-generators";

export default function processTaskWithPool<T>(
tasks: Array<() => Promise<T>>,
export default function* processTaskWithPool(
tasks: Array<() => Saga<void>>,
poolSize: number,
): Promise<Array<T>> {
return new Promise((resolve, reject) => {
const promises = [];
let isFinalResolveScheduled = false;
): Saga<void> {
const startedTasks: Array<Task<void>> = [];
let isFinalResolveScheduled = false;
let error = null;

const startNextTask = () => {
if (tasks.length === 0) {
if (!isFinalResolveScheduled) {
isFinalResolveScheduled = true;
function* forkSafely(fn): Saga<void> {
// Errors from forked tasks cannot be caught, see https://redux-saga.js.org/docs/advanced/ForkModel/#error-propagation
// However, the task pool should not abort if a single task fails.
// Therefore, use this wrapper to safely execute all tasks and possibly rethrow the last error in the end.
try {
yield* call(fn);
} catch (e) {
error = e;
}
}

function* startNextTask(): Saga<void> {
if (tasks.length === 0) {
if (!isFinalResolveScheduled) {
isFinalResolveScheduled = true;

// All tasks were kicked off, which is why all promises can be
// awaited now together.
Promise.all(promises).then(resolve, reject);
}
return;
// All tasks were kicked off, which is why all tasks can be
// awaited now together.
yield* join(startedTasks);
if (error != null) throw error;
}
return;
}

const task = tasks.shift();
const newPromise = task();
promises.push(newPromise);
const task = tasks.shift();
const newTask = yield* fork(forkSafely, task);
startedTasks.push(newTask);

// If that promise is done, process a new one (that way,
// the pool size stays constant until the queue is almost empty.)
newPromise.then(startNextTask, startNextTask);
};
// If that task is done, process a new one (that way,
// the pool size stays constant until the queue is almost empty.)
yield* join(newTask);
yield* call(startNextTask);
}

for (let i = 0; i < poolSize; i++) {
startNextTask();
}
});
for (let i = 0; i < poolSize; i++) {
yield* fork(startNextTask);
}
// The saga will wait for all forked tasks to terminate before returning, because
// fork() creates attached forks (in contrast to spawn()).
}
13 changes: 9 additions & 4 deletions frontend/javascripts/libs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export function mod(x: number, n: number) {
return ((x % n) + n) % n;
}

export function values<K, V>(o: { [K]: V }): Array<V> {
// $FlowIssue[incompatible-return] remove once https://github.com/facebook/flow/issues/2221 is fixed
return Object.values(o);
}

export function map2<A, B>(fn: (A, number) => B, tuple: [A, A]): [B, B] {
const [x, y] = tuple;
return [fn(x, 0), fn(y, 1)];
Expand Down Expand Up @@ -514,8 +519,8 @@ export function filterWithSearchQueryOR<T: { +[string]: mixed }, P: $Keys<T>>(
_.some(properties, fieldName => {
const value = typeof fieldName === "function" ? fieldName(model) : model[fieldName];
if (value != null && (typeof value === "string" || value instanceof Object)) {
const values = getRecursiveValues(value);
return _.some(values, v => v != null && v.toString().match(regexp));
const recursiveValues = getRecursiveValues(value);
return _.some(recursiveValues, v => v != null && v.toString().match(regexp));
} else {
return false;
}
Expand Down Expand Up @@ -545,8 +550,8 @@ export function filterWithSearchQueryAND<T: { +[string]: mixed }, P: $Keys<T>>(
_.some(properties, fieldName => {
const value = typeof fieldName === "function" ? fieldName(model) : model[fieldName];
if (value !== null && (typeof value === "string" || value instanceof Object)) {
const values = getRecursiveValues(value);
return _.some(values, v => v != null && v.toString().match(pattern));
const recursiveValues = getRecursiveValues(value);
return _.some(recursiveValues, v => v != null && v.toString().match(pattern));
} else {
return false;
}
Expand Down
27 changes: 14 additions & 13 deletions frontend/javascripts/oxalis/api/api_latest.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ import {
} from "oxalis/model/helpers/position_converter";
import { callDeep } from "oxalis/view/right-border-tabs/tree_hierarchy_view_helpers";
import { centerTDViewAction } from "oxalis/model/actions/view_mode_actions";
import { changeActiveIsosurfaceCellAction } from "oxalis/model/actions/segmentation_actions";
import {
loadAdHocMeshAction,
loadPrecomputedMeshAction,
} from "oxalis/model/actions/segmentation_actions";
import { discardSaveQueuesAction } from "oxalis/model/actions/save_actions";
import {
doWithToken,
Expand Down Expand Up @@ -79,10 +82,7 @@ import {
getRotation,
getRequestLogZoomStep,
} from "oxalis/model/accessors/flycam_accessor";
import {
loadMeshFromFile,
maybeFetchMeshFiles,
} from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper";
import { maybeFetchMeshFiles } from "oxalis/view/right-border-tabs/segments_tab/segments_view_helper";
import { overwriteAction } from "oxalis/model/helpers/overwrite_action_middleware";
import { parseNml } from "oxalis/model/helpers/nml_helpers";
import { rotate3DViewTo } from "oxalis/controller/camera_controller";
Expand Down Expand Up @@ -140,6 +140,7 @@ import * as Utils from "libs/utils";
import dimensions from "oxalis/model/dimensions";
import messages from "messages";
import window, { location } from "libs/window";
import DataLayer from "oxalis/model/data_layer";

type OutdatedDatasetConfigurationKeys = "segmentationOpacity" | "isSegmentationDisabled";

Expand Down Expand Up @@ -1009,9 +1010,7 @@ class DataApi {
*/
async reloadBuckets(layerName: string): Promise<void> {
await Promise.all(
Object.keys(this.model.dataLayers).map(async currentLayerName => {
const dataLayer = this.model.dataLayers[currentLayerName];

Utils.values(this.model.dataLayers).map(async (dataLayer: DataLayer) => {
if (dataLayer.name === layerName) {
if (dataLayer.cube.isSegmentation) {
await Model.ensureSavedState();
Expand All @@ -1030,7 +1029,7 @@ class DataApi {
if (hasVolumeTracings(Store.getState().tracing)) {
await Model.ensureSavedState();
}
_.forEach(this.model.dataLayers, dataLayer => {
Utils.values(this.model.dataLayers).forEach((dataLayer: DataLayer) => {
dataLayer.cube.collectAllBuckets();
dataLayer.layerRenderingManager.refresh();
});
Expand Down Expand Up @@ -1619,9 +1618,9 @@ class DataApi {
* const availableMeshFiles = await api.data.getAvailableMeshFiles();
* api.data.setActiveMeshFile(availableMeshFiles[0]);
*
* await api.data.loadPrecomputedMesh(segmentId, currentPosition);
* api.data.loadPrecomputedMesh(segmentId, currentPosition);
*/
async loadPrecomputedMesh(segmentId: number, seedPosition: Vector3, layerName: ?string) {
loadPrecomputedMesh(segmentId: number, seedPosition: Vector3, layerName: ?string) {
const state = Store.getState();
const effectiveLayerName = getNameOfRequestedOrVisibleSegmentationLayer(state, layerName);
if (!effectiveLayerName) {
Expand Down Expand Up @@ -1654,7 +1653,9 @@ class DataApi {
}
}

await loadMeshFromFile(segmentId, seedPosition, meshFileName, segmentationLayer, dataset);
Store.dispatch(
daniel-wer marked this conversation as resolved.
Show resolved Hide resolved
loadPrecomputedMeshAction(segmentId, seedPosition, meshFileName, effectiveLayerName),
);
}

/**
Expand All @@ -1666,7 +1667,7 @@ class DataApi {
* api.data.computeMeshOnDemand(segmentId, currentPosition);
*/
computeMeshOnDemand(segmentId: number, seedPosition: Vector3) {
Store.dispatch(changeActiveIsosurfaceCellAction(segmentId, seedPosition, true));
Store.dispatch(loadAdHocMeshAction(segmentId, seedPosition));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/oxalis/api/cross_origin_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const onMessage = async event => {
case "loadPrecomputedMesh": {
const segmentId = args[0];
const seedPosition = args[1];
await api.data.loadPrecomputedMesh(segmentId, seedPosition);
api.data.loadPrecomputedMesh(segmentId, seedPosition);
break;
}
case "setMeshVisibility": {
Expand Down
73 changes: 69 additions & 4 deletions frontend/javascripts/oxalis/controller/url_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { V3 } from "libs/mjs";
import { applyState } from "oxalis/model_initialization";
import { getRotation, getPosition } from "oxalis/model/accessors/flycam_accessor";
import { getSkeletonTracing, getActiveNode } from "oxalis/model/accessors/skeletontracing_accessor";
import Store, { type OxalisState, type MappingType } from "oxalis/store";
import Store, {
type OxalisState,
type MappingType,
type IsosurfaceInformation,
} from "oxalis/store";
import * as Utils from "libs/utils";
import constants, {
type ViewMode,
Expand All @@ -22,16 +26,63 @@ import { validateUrlStateJSON } from "types/validation";

const MAX_UPDATE_INTERVAL = 1000;

type BaseMeshUrlDescriptor = {|
+segmentId: number,
+seedPosition: Vector3,
|};
type AdHocMeshUrlDescriptor = {|
...BaseMeshUrlDescriptor,
+isPrecomputed: false,
mappingName: ?string,
mappingType: ?MappingType,
|};
type PrecomputedMeshUrlDescriptor = {|
...BaseMeshUrlDescriptor,
+isPrecomputed: true,
meshFileName: string,
|};
type MeshUrlDescriptor = AdHocMeshUrlDescriptor | PrecomputedMeshUrlDescriptor;

export type UrlStateByLayer = {
[layerName: string]: {
meshInfo?: {
meshFileName: ?string,
meshes: Array<MeshUrlDescriptor>,
},
mappingInfo?: {
mappingName: string,
mappingType: MappingType,
agglomerateIdsToImport?: [number],
agglomerateIdsToImport?: Array<number>,
},
},
};

function mapIsosurfaceInfoToUrlMeshDescriptor(
isosurfaceInfo: IsosurfaceInformation,
): MeshUrlDescriptor {
const { segmentId, seedPosition } = isosurfaceInfo;
const baseUrlDescriptor: BaseMeshUrlDescriptor = {
segmentId,
seedPosition: V3.floor(seedPosition),
};
if (isosurfaceInfo.isPrecomputed) {
const { meshFileName } = isosurfaceInfo;
return {
...baseUrlDescriptor,
isPrecomputed: true,
meshFileName,
};
} else {
const { mappingName, mappingType } = isosurfaceInfo;
return {
...baseUrlDescriptor,
isPrecomputed: false,
mappingName,
mappingType,
};
}
}

// If the type of UrlManagerState changes, the following files need to be updated:
// docs/sharing.md#sharing-link-format
// frontend/javascripts/types/schemas/url_state.schema.js
Expand Down Expand Up @@ -173,14 +224,28 @@ class UrlManager {
.map(node => ({ activeNode: node.id }))
.getOrElse({});

const stateByLayer = {};
const stateByLayer: UrlStateByLayer = {};
for (const layerName of Object.keys(state.temporaryConfiguration.activeMappingByLayer)) {
const mappingInfo = state.temporaryConfiguration.activeMappingByLayer[layerName];
if (mappingInfo.mappingStatus === MappingStatusEnum.ENABLED) {
if (
mappingInfo.mappingStatus === MappingStatusEnum.ENABLED &&
mappingInfo.mappingName != null
) {
const { mappingName, mappingType } = mappingInfo;
stateByLayer[layerName] = { mappingInfo: { mappingName, mappingType } };
}
}
for (const layerName of Object.keys(state.localSegmentationData)) {
const { isosurfaces, currentMeshFile } = state.localSegmentationData[layerName];
const currentMeshFileName = currentMeshFile?.meshFileName;
const meshes = Utils.values(isosurfaces)
.filter(({ isVisible }) => isVisible)
.map(mapIsosurfaceInfoToUrlMeshDescriptor);

if (currentMeshFileName != null || meshes.length > 0) {
stateByLayer[layerName] = { meshInfo: { meshFileName: currentMeshFileName, meshes } };
}
}
const stateByLayerOptional = _.size(stateByLayer) > 0 ? { stateByLayer } : {};

// $FlowIssue[incompatible-exact] See https://github.com/facebook/flow/issues/2977
Expand Down
Loading