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

Support non-quadratic viewports #3634

Merged
merged 36 commits into from
Feb 18, 2019
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4b56e7a
allow viewports to be non-quadratic (3D viewport still a bit broken; …
philippotto Jan 16, 2019
0e6c7a6
zoom into viewports so that data cannot be cropped
philippotto Jan 16, 2019
f38a139
fix interactions with 3D viewport
philippotto Jan 16, 2019
d6c75af
make arbitrary viewport square to avoid scaling problems
philippotto Jan 18, 2019
60cd536
Merge branch 'master' into non-quadratic-viewports
philippotto Jan 18, 2019
c255453
fix node selection and other rectangular-viewport-related bugs
philippotto Jan 21, 2019
a5fa735
fix panning
philippotto Jan 21, 2019
3c8e2e8
iterate on brush
philippotto Jan 21, 2019
4b9a24f
fix brush preview
philippotto Jan 21, 2019
85deff7
clean up
philippotto Jan 21, 2019
ea3aba3
remove viewport width from info tab view and clean up info tab UI
philippotto Jan 21, 2019
586e6e3
remove unused constants
philippotto Jan 21, 2019
61f1820
use larger viewport area to render more data instead of upscaling dat…
philippotto Jan 23, 2019
b9ce445
Merge branch 'master' of github.com:scalableminds/webknossos into non…
philippotto Jan 28, 2019
4c374ec
adapt mouse interactions to true-viewport-scaling; fix fallback rende…
philippotto Jan 28, 2019
cd5b18b
clean up
philippotto Jan 29, 2019
abec2f0
disable some CI checks
philippotto Jan 29, 2019
5992bc7
Merge branch 'master' of github.com:scalableminds/webknossos into non…
philippotto Feb 1, 2019
e5d48d9
Merge branch 'master' of github.com:scalableminds/webknossos into non…
philippotto Feb 7, 2019
134cd42
clean up according to PR feedback
philippotto Feb 8, 2019
0e7eb19
update changelog
philippotto Feb 8, 2019
9df2531
clean up
philippotto Feb 8, 2019
cb7b5d0
fix scalebars and implement tooltips for complete viewport extent
philippotto Feb 8, 2019
887afb6
Refactor bucket counting (#3750)
philippotto Feb 11, 2019
13dd17d
Merge branch 'master' of github.com:scalableminds/webknossos into non…
philippotto Feb 12, 2019
9d13e6a
remove obsolete comment
philippotto Feb 12, 2019
1c4a207
Merge branch 'master' of github.com:scalableminds/webknossos into non…
philippotto Feb 12, 2019
38d4249
fix import
philippotto Feb 12, 2019
7e22dc6
Merge branch 'master' of github.com:scalableminds/webknossos into non…
philippotto Feb 18, 2019
613a64d
revert not-on-master.sh
philippotto Feb 18, 2019
49173d5
remove unnecessary import
philippotto Feb 18, 2019
f41e386
fix tests and refactor to remove cyclic dependencies in accessors
philippotto Feb 18, 2019
7d93921
extract constant
philippotto Feb 18, 2019
02ce143
update screenshots
philippotto Feb 18, 2019
4e7e3d7
adapt screenshot view value
philippotto Feb 18, 2019
40270c4
Merge branch 'master' of github.com:scalableminds/webknossos into non…
philippotto Feb 18, 2019
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
8 changes: 4 additions & 4 deletions .circleci/not-on-master.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
set -Eeuo pipefail

if [ "${CIRCLE_BRANCH}" == "master" ]; then
# if [ "${CIRCLE_BRANCH}" == "master" ]; then
philippotto marked this conversation as resolved.
Show resolved Hide resolved
echo "Skipping this step on master..."
else
exec "$@"
fi
# else
# exec "$@"
# fi
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md).
- Added an UI to select a mapping for a segmentation layer. The UI is placed in the segmentation tab within the tracing view. [#3720](https://github.com/scalableminds/webknossos/pull/3720)

### Changed
- Data rendering is not tied to square viewports, anymore. As a result the screen space is used more efficiently to show data. Also, increasing the size of a viewport will result in more data being rendered (as opposed to the same data will be upscaled). [#3634](https://github.com/scalableminds/webknossos/pull/3634)
- Mappings for segmentations will be read automatically from the file system. It's not necessary to define the mappings within the `datasource-properties.json`, anymore. [#3720](https://github.com/scalableminds/webknossos/pull/3720)
- The active node is highlighted with a "halo ring". Additionally, the node is also rendered as a circle. In flight and oblique modes the halo is hidden. [#3731](https://github.com/scalableminds/webknossos/pull/3731)

Expand Down
29 changes: 29 additions & 0 deletions flow-typed/npm/js-priority-queue_vx.x.x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// flow-typed signature: f7c845aa1ded4959c457c2cb20ea95f5
// flow-typed version: <<STUB>>/js-priority-queue_v0.1.5/flow_v0.91.0

/**
* This is an autogenerated libdef stub for:
*
* 'js-priority-queue'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/

declare module "js-priority-queue" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😇

declare class PriorityQueue<T> {
length: number;

constructor(
options?: $Shape<{ comparator: (T, T) => number, initialValues: Array<T> }>,
): PriorityQueue<T>;
queue(value: T): void;
peek(): T;
dequeue(): T;
clear(): void;
}
declare module.exports: typeof PriorityQueue;
}
46 changes: 46 additions & 0 deletions frontend/javascripts/libs/ThreeDMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @flow

import type { Vector3 } from "oxalis/constants";

// This is a Map datastructure for which the key
// is a Vector3.
export default class ThreeDMap<T> {
map: Map<number, ?Map<number, ?Map<number, T>>>;

constructor() {
this.map = new Map();
}

get(vec: Vector3): ?T {
const [x, y, z] = vec;
const atX = this.map.get(x);
if (atX == null) {
return null;
}
const atY = atX.get(y);
if (atY == null) {
return null;
}
return atY.get(z);
}

set(vec: Vector3, value: T): void {
const [x, y, z] = vec;
if (this.map.get(x) == null) {
this.map.set(x, new Map());
}
// Flow doesn't understand that the access to X
// is guaranteed to be not null due to the above code.
// $FlowFixMe
if (this.map.get(x).get(y) == null) {
// $FlowFixMe
this.map.get(x).set(y, new Map());
}

// $FlowFixMe
this.map
.get(x)
.get(y)
.set(z, value);
}
}
6 changes: 4 additions & 2 deletions frontend/javascripts/libs/format_utils.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// @flow
import moment from "moment";

import type { Vector3, Vector6 } from "oxalis/constants";
import { Unicode, type Vector3, type Vector6 } from "oxalis/constants";
import * as Utils from "libs/utils";

const { ThinSpace, MultiplicationSymbol } = Unicode;

const COLOR_MAP: Array<string> = [
"#6962C5",
"#403C78",
Expand Down Expand Up @@ -40,7 +42,7 @@ export function formatTuple(tuple: ?(Array<number> | Vector3 | Vector6)) {
export function formatScale(scaleArr: Vector3): string {
if (scaleArr != null && scaleArr.length > 0) {
const scaleArrRounded = scaleArr.map(value => Utils.roundTo(value, 2));
return `${scaleArrRounded.join(" × ")} nm³`;
return `${scaleArrRounded.join(ThinSpace + MultiplicationSymbol + ThinSpace)} nm³`;
} else {
return "";
}
Expand Down
5 changes: 5 additions & 0 deletions frontend/javascripts/libs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import window, { document, location } from "libs/window";
export type Comparator<T> = (T, T) => -1 | 0 | 1;
type UrlParams = { [key: string]: string };

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)];
}

export function map3<A, B>(fn: (A, number) => B, tuple: [A, A, A]): [B, B, B] {
const [x, y, z] = tuple;
return [fn(x, 0), fn(y, 1), fn(z, 2)];
Expand Down
39 changes: 26 additions & 13 deletions frontend/javascripts/oxalis/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* @flow
*/

export const ModeValues = ["orthogonal", "flight", "oblique", "volume"]; // MODE_PLANE_TRACING | MODE_ARBITRARY | MODE_ARBITRARY_PLANE | MODE_VOLUME
export const ModeValuesIndices = { Orthogonal: 0, Flight: 1, Oblique: 2, Volume: 3 };
export type Mode = "orthogonal" | "oblique" | "flight" | "volume";
export const ViewModeValues = ["orthogonal", "flight", "oblique", "volume"]; // MODE_PLANE_TRACING | MODE_ARBITRARY | MODE_ARBITRARY_PLANE | MODE_VOLUME
export const ViewModeValuesIndices = { Orthogonal: 0, Flight: 1, Oblique: 2, Volume: 3 };
export type ViewMode = "orthogonal" | "oblique" | "flight" | "volume";
export type Vector2 = [number, number];
export type Vector3 = [number, number, number];
export type Vector4 = [number, number, number, number];
Expand Down Expand Up @@ -39,6 +39,8 @@ export const OrthoViews = {
};
export type OrthoView = $Keys<typeof OrthoViews>;
export type OrthoViewMap<T> = { [key: OrthoView]: T };
export type OrthoViewExtents = $ReadOnly<OrthoViewMap<Vector2>>;
export type OrthoViewRects = $ReadOnly<OrthoViewMap<Rect>>;

export const ArbitraryViewport = "arbitraryViewport";
export const ArbitraryViews = {
Expand All @@ -49,6 +51,11 @@ export type ArbitraryView = $Keys<typeof ArbitraryViews>;
export type ArbitraryViewMap<T> = { [key: ArbitraryView]: T };

export type Viewport = OrthoView | typeof ArbitraryViewport;

export type ViewportMap<T> = { [key: Viewport]: T };
export type ViewportExtents = $ReadOnly<ViewportMap<Vector2>>;
export type ViewportRects = $ReadOnly<ViewportMap<Rect>>;

export const OrthoViewValues: Array<OrthoView> = Object.keys(OrthoViews);
export const OrthoViewIndices = {
PLANE_XY: OrthoViewValues.indexOf("PLANE_XY"),
Expand Down Expand Up @@ -111,10 +118,21 @@ export const POSITION_REF_REGEX = /#\(([0-9]+,[0-9]+,[0-9]+)\)/g;
// There is an outer yellow CSS border and an inner (red/green/blue) border
// that is a result of the plane being smaller than the renderer viewport
export const OUTER_CSS_BORDER = 2;
const INNER_RENDERER_BORDER = 2;
export const ORTHOGONAL_BORDER = OUTER_CSS_BORDER + INNER_RENDERER_BORDER;
const PLANE_WIDTH = 376;
const VIEWPORT_WIDTH = PLANE_WIDTH + ORTHOGONAL_BORDER * 2;
const VIEWPORT_WIDTH = 376;
export const ensureSmallerEdge = false;

// Using the following dimensions for the address space,
// the look up buffer (256**2) is used at a rate of ~ 97%
// ((32 × 32 × 50 + 16 × 16 × 50) / 256^2 = 0.976563)
export const addressSpaceDimensions = {
normal: [32, 32, 50],
fallback: [16, 16, 50],
};

export const Unicode = {
ThinSpace: "\u202f",
MultiplicationSymbol: "×",
};

const Constants = {
ARBITRARY_VIEW: 4,
Expand All @@ -131,19 +149,14 @@ const Constants = {

BUCKET_WIDTH: 32,
BUCKET_SIZE: 32 ** 3,
PLANE_WIDTH,
VIEWPORT_WIDTH,
// The size of the gap between the 4 viewports in the orthogonal mode
VIEWPORT_GAP_WIDTH: 20,

// We require at least 3 * 512 === 1536 buckets per data layer to fit onto the GPU.
// This number is used during setup to pick appropriate data texture sizes.
// Previously, a minimum of 1200 buckets was enforced. Since, this limit required at least
// three 4096**2 textures, a minimum of capacity of 1536 is actually reasonable.
MINIMUM_REQUIRED_BUCKET_CAPACITY: 3 * 512,
DISTANCE_3D: 140,
MAX_TEXTURE_COUNT_PER_LAYER: 1,
LOOK_UP_TEXTURE_WIDTH: 128,
LOOK_UP_TEXTURE_WIDTH: 256,

TDView_MOVE_SPEED: 150,
MIN_MOVE_VALUE: 30,
Expand Down
6 changes: 3 additions & 3 deletions frontend/javascripts/oxalis/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import UrlManager from "oxalis/controller/url_manager";
import * as Utils from "libs/utils";
import api from "oxalis/api/internal_api";
import app from "app";
import constants, { ControlModeEnum, type Mode } from "oxalis/constants";
import constants, { ControlModeEnum, type ViewMode } from "oxalis/constants";
import messages from "messages";
import window, { document } from "libs/window";

Expand All @@ -41,7 +41,7 @@ type OwnProps = {|
initialCommandType: TraceOrViewCommand,
|};
type StateProps = {|
viewMode: Mode,
viewMode: ViewMode,
|};
type Props = {| ...OwnProps, ...StateProps |};
type PropsWithRouter = {| ...Props, history: RouterHistory |};
Expand Down Expand Up @@ -274,7 +274,7 @@ class Controller extends React.PureComponent<PropsWithRouter, State> {
/>
);
}
const allowedModes = Store.getState().tracing.restrictions.allowedModes;
const { allowedModes } = Store.getState().tracing.restrictions;
const mode = this.props.viewMode;

if (!allowedModes.includes(mode)) {
Expand Down
61 changes: 44 additions & 17 deletions frontend/javascripts/oxalis/controller/camera_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ import * as THREE from "three";
import TWEEN from "tween.js";
import _ from "lodash";

import { getInputCatcherAspectRatio } from "oxalis/model/accessors/view_mode_accessor";
import { getBoundaries } from "oxalis/model/accessors/dataset_accessor";
import { getPosition } from "oxalis/model/accessors/flycam_accessor";
import {
getPlaneExtentInVoxelFromStore,
getPosition,
} from "oxalis/model/accessors/flycam_accessor";
import { listenToStoreProperty } from "oxalis/model/helpers/listener_helpers";
import { setTDCameraAction } from "oxalis/model/actions/view_mode_actions";
import { voxelToNm, getBaseVoxel } from "oxalis/model/scaleinfo";
import Dimensions from "oxalis/model/dimensions";
import Store, { type CameraData } from "oxalis/store";
import api from "oxalis/api/internal_api";
import constants, {
import {
type OrthoView,
type OrthoViewMap,
OrthoViewValuesWithoutTDView,
Expand Down Expand Up @@ -57,17 +61,22 @@ class CameraController extends React.PureComponent<Props> {

updateCamViewport(): void {
const state = Store.getState();
const clippingDistance = state.userConfiguration.clippingDistance;
const { clippingDistance } = state.userConfiguration;
const scaleFactor = getBaseVoxel(state.dataset.dataSource.scale);
const zoom = state.flycam.zoomStep;
const halfBoundary = (constants.VIEWPORT_WIDTH / 2) * zoom;
for (const planeId of OrthoViewValuesWithoutTDView) {
const [width, height] = getPlaneExtentInVoxelFromStore(
state,
state.flycam.zoomStep,
planeId,
).map(x => x * scaleFactor);

this.props.cameras[planeId].left = -width / 2;
this.props.cameras[planeId].right = width / 2;

this.props.cameras[planeId].bottom = -height / 2;
this.props.cameras[planeId].top = height / 2;

this.props.cameras[planeId].near = -clippingDistance;
const scaledBoundary = halfBoundary * scaleFactor;
this.props.cameras[planeId].left = -scaledBoundary;
this.props.cameras[planeId].bottom = -scaledBoundary;
this.props.cameras[planeId].right = scaledBoundary;
this.props.cameras[planeId].top = scaledBoundary;
this.props.cameras[planeId].updateProjectionMatrix();
}
}
Expand All @@ -94,6 +103,10 @@ class CameraController extends React.PureComponent<Props> {
storeState => storeState.flycam.zoomStep,
() => this.updateCamViewport(),
),
listenToStoreProperty(
storeState => storeState.viewModeData.plane.inputCatcherRects,
() => this.updateCamViewport(),
),
listenToStoreProperty(
storeState => storeState.flycam.currentMatrix,
() => this.update(),
Expand Down Expand Up @@ -149,6 +162,8 @@ export function rotate3DViewTo(id: OrthoView, animate: boolean = true): void {
const b = voxelToNm(dataset.dataSource.scale, getBoundaries(dataset).upperBoundary);
const pos = voxelToNm(dataset.dataSource.scale, getPosition(state.flycam));

const aspectRatio = getInputCatcherAspectRatio(state, OrthoViews.TDView);

let to: TweenState;
if (id === OrthoViews.TDView) {
const diagonal = Math.sqrt(b[0] * b[0] + b[1] * b[1]);
Expand All @@ -175,21 +190,33 @@ export function rotate3DViewTo(id: OrthoView, animate: boolean = true): void {
// Calulate the x coordinate so that the vector from the camera to the cube's middle point is
// perpendicular to the vector going from (0, b[1], 0) to (b[0], 0, 0).

const squareLeft = -distance - padding;
const squareRight = diagonal - distance + padding;
const squareTop = diagonal / 2 + padding + yOffset;
const squareBottom = -diagonal / 2 - padding + yOffset;
const squareCenterX = (squareLeft + squareRight) / 2;
const squareCenterY = (squareTop + squareBottom) / 2;
const squareWidth = Math.abs(squareLeft - squareRight);

const height = squareWidth / aspectRatio;

to = {
dx: b[1] / diagonal,
dy: b[0] / diagonal,
dz: -1 / 2,
upX: 0,
upY: 0,
upZ: -1,
l: -distance - padding,
r: diagonal - distance + padding,
t: diagonal / 2 + padding + yOffset,
b: -diagonal / 2 - padding + yOffset,
l: squareCenterX - squareWidth / 2,
r: squareCenterX + squareWidth / 2,
t: squareCenterY + height / 2,
b: squareCenterY - height / 2,
};
} else {
const ind = Dimensions.getIndices(id);
const width = Math.max(b[ind[0]], b[ind[1]] * 1.12) * 1.1;
const height = width / aspectRatio;

const paddingTop = width * 0.12;
const padding = ((width / 1.1) * 0.1) / 2;
const offsetX = pos[ind[0]] + padding + (width - b[ind[0]]) / 2;
Expand Down Expand Up @@ -219,11 +246,11 @@ export function rotate3DViewTo(id: OrthoView, animate: boolean = true): void {
l,
t,
r: l + width,
b: t - width,
b: t - height,
};
}

const updateCameraTDView = function(tweenState: TweenState): void {
const updateCameraTDView = (tweenState: TweenState) => {
const p = voxelToNm(
Store.getState().dataset.dataSource.scale,
getPosition(Store.getState().flycam),
Expand Down Expand Up @@ -265,7 +292,7 @@ export function rotate3DViewTo(id: OrthoView, animate: boolean = true): void {
.to(to, time)
.onUpdate(function updater() {
// TweenJS passes the current state via the `this` object.
// However, for easier type checking, we pass it as an explicit
// However, for better type checking, we pass it as an explicit
// parameter.
updateCameraTDView(this);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function onClick(
const pickingScene = new THREE.Scene();
pickingScene.add(pickingNode);

let { width, height } = getInputCatcherRect(plane);
let { width, height } = getInputCatcherRect(Store.getState(), plane);
width = Math.round(width);
height = Math.round(height);

Expand All @@ -150,8 +150,8 @@ function onClick(
const borderWidth = OUTER_CSS_BORDER;
const [x, y] = [Math.round(position.x) - borderWidth, Math.round(position.y) - borderWidth];
// compute the index of the pixel under the cursor,
// while inverting along the y-axis, because OpenGL has its origin bottom-left :/
const index = (x + (width - y) * height) * 4;
// while inverting along the y-axis, because WebGL has its origin bottom-left :/
const index = (x + (height - y) * width) * 4;
// the nodeId can be reconstructed by interpreting the RGB values of the pixel as a base-255 number
const nodeId = buffer.subarray(index, index + 3).reduce((a, b) => a * 255 + b, 0);
SceneController.skeleton.stopPicking();
Expand Down
Loading