diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 7693779430f..9b05c4b0116 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -11,6 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released [Commits](https://github.com/scalableminds/webknossos/compare/24.04.0...HEAD) ### Added +- Creating and deleting edges is now possible with ctrl+(alt/shift)+leftclick in orthogonal, flight and oblique mode. Also, the flight and oblique modes allow selecting nodes with leftclick, creating new trees with 'c' and deleting the active node with 'del'. [#7678](https://github.com/scalableminds/webknossos/pull/7678) ### Changed - Improved task list to sort tasks by project date, add option to expand all tasks at once and improve styling. [#7709](https://github.com/scalableminds/webknossos/pull/7709) @@ -19,6 +20,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Duplicated annotations are opened in a new browser tab. [#7724](https://github.com/scalableminds/webknossos/pull/7724) ### Fixed +- Fixed that the Command modifier on MacOS wasn't treated correctly for some shortcuts. Also, instead of the Alt key, the ⌥ key is shown as a hint in the status bar on MacOS. [#7659](https://github.com/scalableminds/webknossos/pull/7659) - Moving from the time tracking overview to its detail view, the selected user was not preselected in the next view. [#7722](https://github.com/scalableminds/webknossos/pull/7722) ### Removed diff --git a/docs/keyboard_shortcuts.md b/docs/keyboard_shortcuts.md index 1f19b80e938..93c96181ad4 100644 --- a/docs/keyboard_shortcuts.md +++ b/docs/keyboard_shortcuts.md @@ -9,11 +9,11 @@ A complete listing of all available keyboard & mouse shortcuts for WEBKNOSSOS ca | Key Binding | Operation | | ----------------------------- | ------------------------------------------- | -| CTRL / CMD + Z | Undo | -| CTRL / CMD + Y | Redo | -| CTRL / CMD + S | Save | -| I or CTRL + Mousewheel | Zoom In | -| O or CTRL + Mousewheel | Zoom Out | +| CTRL/CMD + Z | Undo | +| CTRL/CMD + Y | Redo | +| CTRL/CMD + S | Save | +| I or CTRL/CMD + Mousewheel | Zoom In | +| O or CTRL/CMD + Mousewheel | Zoom Out | | P | Select Previous Comment | | N | Select Next Comment | | 3 | Toggle Segmentation Opacity | @@ -31,7 +31,7 @@ A complete listing of all available keyboard & mouse shortcuts for WEBKNOSSOS ca | 1 | Toggle Visibility of all Trees | | 2 | Toggle Visibility of Inactive Trees | | SHIFT + Mousewheel | Change Node Radius | -| CTRL + SHIFT + F | Open Tree Search (if Tree List is visible) | +| CTRL/CMD + SHIFT + F | Open Tree Search (if Tree List is visible) | | F or Mousewheel | Move Forward by a Single Slice | | D or Mousewheel | Move Backward by a Single Slice | @@ -48,16 +48,18 @@ Note that skeleton-specific mouse actions are usually only available when the sk | Right-Click Drag (3D View) | Rotate 3D View | | Left Click | Create New Node | | Left Click | Select Node (Mark as Active Node) under cursor | -| Left Drag | Move node under cursor | -| Right Click (on node) | Bring up the context-menu with further actions | +| Left Drag | Move node under cursor | +| Right Click (on node) | Bring up the context-menu with further actions | +| SHIFT + ALT + Left Click | Merge Two Nodes and Combine Trees | +| SHIFT + CTRL/CMD + Left Click | Delete Edge / Split Trees | | C | Create New Tree | -| CTRL + . | Navigate to the next Node (Mark as Active)| -| CTRL + , | Navigate to previous Node (Mark as Active) | -| CTRL + Left Click / CTRL + Arrow Keys | Move the Active Node | -| Del | Delete Node / Split Trees | -| B | Mark Node as New Branchpoint | -| J | Jump To Last Branchpoint | -| S | Center Camera on Active Node | +| CTRL/CMD + . | Navigate to the next Node (Mark as Active) | +| CTRL/CMD + , | Navigate to previous Node (Mark as Active) | +| CTRL/CMD + Left Click / CTRL/CMD + Arrow Keys | Move the Active Node | +| Del | Delete Node / Split Trees | +| B | Mark Node as New Branchpoint | +| J | Jump To Last Branchpoint | +| S | Center Camera on Active Node | Note that you can enable *Classic Controls* which will behave slightly different and more explicit for the mouse actions: @@ -66,18 +68,16 @@ Note that you can enable *Classic Controls* which will behave slightly different | ----------------------------- | ------------- | | Right Click | Create New Node | | SHIFT + Left Click | Select Node (Mark as Active Node) | -| SHIFT + Left Click | Select Node (Mark as Active Node) | -| SHIFT + ALT + Left Click | Merge Two Nodes and Combine Trees | -| SHIFT + CTRL + Left Click | Delete Edge / Split Trees | ### Flight / Oblique Mode | Key Binding | Operation | | ----------------------------- | ------------------------------------------ | +| Left Click | Select Node (Mark as Active Node) under cursor | | Left Mouse Drag or Arrow Keys | Rotation | | SPACE | Move Forward | -| CTRL + SPACE | Move Backward | +| CTRL/CMD + SPACE | Move Backward | | I, O | Zoom In And Out | | SHIFT + Arrow | Rotation Around Axis | | R | Invert Direction | @@ -86,8 +86,10 @@ Note that you can enable *Classic Controls* which will behave slightly different | S | Center Active Node | | F | Forward Without Recording Waypoints | | D | Backward Without Recording Waypoints | +| Del | Delete Node / Split Trees | | SHIFT + SPACE | Delete Active Node, Recenter Previous Node | - +| SHIFT + ALT + Left Click | Merge Two Nodes and Combine Trees | +| SHIFT + CTRL/CMD + Left Click | Delete Edge / Split Trees | ## Volume Mode @@ -96,8 +98,8 @@ Note that you can enable *Classic Controls* which will behave slightly different | Left Mouse Drag or Arrow Keys | Move (Move Mode) / Add To Current Segment (Trace / Brush Mode) | | Right Click | Bring up context-menu with further actions | | SHIFT + Left Click | Select Active Segment | -| CTRL + Left Mouse Drag | Add Voxels To Current Segment while inverting the overwrite-mode (see toolbar for overwrite-mode) | -| CTRL + SHIFT + Left Mouse Drag | Remove Voxels From Segment | +| CTRL/CMD + Left Mouse Drag | Add Voxels To Current Segment while inverting the overwrite-mode (see toolbar for overwrite-mode) | +| CTRL/CMD + SHIFT + Left Mouse Drag | Remove Voxels From Segment | | Alt + Mouse Move | Move | | C | Create New Segment | | SHIFT + Mousewheel or SHIFT + I, O | Change Brush Size (Brush Mode) | @@ -108,44 +110,44 @@ Note that you can enable *Classic Controls* which won't open a context menu on r | Key Binding | Operation | | --------------------------------- | ----------------------------------------------------------- | | Right Mouse Drag | Remove Voxels | -| CTRL + Right Mouse Drag | Remove Voxels while inverting the overwrite-mode (see toolbar for overwrite-mode) | +| CTRL/CMD + Right Mouse Drag | Remove Voxels while inverting the overwrite-mode (see toolbar for overwrite-mode) | ## Tool Switching Shortcuts -Note that you need to first press CTRL + K, release these keys and then press the letter that was assigned to a specific tool in order to switch to it. -CTRL + K is not needed for cyclic tool switching via W / SHIFT + W. +Note that you need to first press CTRL/CMD + K, release these keys and then press the letter that was assigned to a specific tool in order to switch to it. +CTRL/CMD + K is not needed for cyclic tool switching via W / SHIFT + W. | Key Binding | Operation | | --------------------------------- | --------------------------------------------------------------------------------- | | W | Cycle Through Tools (Move / Skeleton / Trace / Brush / ...) | | SHIFT + W | Cycle Backwards Through Tools (Move / Proofread / Bounding Box / Pick Cell / ...) | -| CTRL + K, **M** | Move Tool | -| CTRL + K, **S** | Skeleton Tool | -| CTRL + K, **B** | Brush Tool | -| CTRL + K, **E** | Brush Erase Tool | -| CTRL + K, **L** | Lasso Tool | -| CTRL + K, **R** | Lasso Erase Tool | -| CTRL + K, **P** | Segment Picker Tool | -| CTRL + K, **Q** | Quick Select Tool | -| CTRL + K, **X** | Bounding Box Tool | -| CTRL + K, **O** | Proofreading Tool | +| CTRL/CMD + K, **M** | Move Tool | +| CTRL/CMD + K, **S** | Skeleton Tool | +| CTRL/CMD + K, **B** | Brush Tool | +| CTRL/CMD + K, **E** | Brush Erase Tool | +| CTRL/CMD + K, **L** | Lasso Tool | +| CTRL/CMD + K, **R** | Lasso Erase Tool | +| CTRL/CMD + K, **P** | Segment Picker Tool | +| CTRL/CMD + K, **Q** | Quick Select Tool | +| CTRL/CMD + K, **X** | Bounding Box Tool | +| CTRL/CMD + K, **O** | Proofreading Tool | ### Brush Related Shortcuts -Note that you need to first press CTRL + K, release these keys and press the suitable number. +Note that you need to first press CTRL/CMD + K, release these keys and press the suitable number. | Key Binding | Operation | | --------------------------------- | --------------------------------------------------------------------------------- | -| CTRL + K, **1** | Switch to small brush | -| CTRL + K, **2** | Switch to medium sized brush | -| CTRL + K, **3** | Switch to large brush | +| CTRL/CMD + K, **1** | Switch to small brush | +| CTRL/CMD + K, **2** | Switch to medium sized brush | +| CTRL/CMD + K, **3** | Switch to large brush | ## Mesh Related Shortcuts | Key Binding | Operation | | ------------------------------------------------------ | ----------------------------------------------------------- | | Shift + Click on a mesh in the 3D viewport | Move the camera to the clicked position | -| Ctrl + Click on a mesh in the 3D viewport | Unload the mesh from WEBKNOSSOS | +| CTRL/CMD + Click on a mesh in the 3D viewport | Unload the mesh from WEBKNOSSOS | ## Agglomerate File Mapping Skeleton diff --git a/frontend/javascripts/libs/input.ts b/frontend/javascripts/libs/input.ts index 2334172a576..b375d1f17c2 100644 --- a/frontend/javascripts/libs/input.ts +++ b/frontend/javascripts/libs/input.ts @@ -20,8 +20,9 @@ import { createNanoEvents, Emitter } from "nanoevents"; // In most cases the heavy lifting is done by libraries in the background. export const KEYBOARD_BUTTON_LOOP_INTERVAL = 1000 / constants.FPS; const MOUSE_MOVE_DELTA_THRESHOLD = 5; -export type ModifierKeys = "alt" | "shift" | "ctrl"; +export type ModifierKeys = "alt" | "shift" | "ctrlOrMeta"; type KeyboardKey = string; +type MouseButton = string; type KeyboardHandler = (event: KeyboardEvent) => void | Promise; // Callable Object, see https://www.typescriptlang.org/docs/handbook/2/functions.html#call-signatures type KeyboardLoopHandler = { @@ -32,13 +33,15 @@ type KeyboardLoopHandler = { }; type KeyboardBindingPress = [KeyboardKey, KeyboardHandler, KeyboardHandler]; type KeyboardBindingDownUp = [KeyboardKey, KeyboardHandler, KeyboardHandler]; -type BindingMap) => any> = Record; +type KeyBindingMap = Record; +type KeyBindingLoopMap = Record; +export type MouseBindingMap = Record; type MouseButtonWhich = 1 | 2 | 3; type MouseButtonString = "left" | "middle" | "right"; -type MouseHandler = +export type MouseHandler = | ((deltaYorX: number, modifier: ModifierKeys | null | undefined) => void) - | ((position: Point2, id: string | null | undefined, event: MouseEvent) => void) - | ((delta: Point2, position: Point2, id: string | null | undefined, event: MouseEvent) => void); + | ((position: Point2, id: string, event: MouseEvent, isTouch: boolean) => void) + | ((delta: Point2, position: Point2, id: string, event: MouseEvent) => void); type HammerJsEvent = { center: Point2; pointers: Array>; @@ -76,11 +79,11 @@ export class InputKeyboardNoLoop { cancelExtendedModeTimeoutId: ReturnType | null = null; constructor( - initialBindings: BindingMap, + initialBindings: KeyBindingMap, options?: { supportInputElements?: boolean; }, - extendedCommands?: BindingMap, + extendedCommands?: KeyBindingMap, ) { if (options) { this.supportInputElements = options.supportInputElements || this.supportInputElements; @@ -125,7 +128,7 @@ export class InputKeyboardNoLoop { }; preventBrowserSearchbarShortcut = (evt: KeyboardEvent) => { - if (evt.ctrlKey && evt.key === "k") { + if ((evt.ctrlKey || evt.metaKey) && evt.key === "k") { evt.preventDefault(); evt.stopPropagation(); } @@ -196,7 +199,7 @@ export class InputKeyboardNoLoop { // It is able to handle key-presses and will continuously // fire the attached callback. export class InputKeyboard { - keyCallbackMap: Record = {}; + keyCallbackMap: KeyBindingLoopMap = {}; keyPressedCount: number = 0; bindings: Array = []; isStarted: boolean = true; @@ -204,7 +207,7 @@ export class InputKeyboard { supportInputElements: boolean = false; constructor( - initialBindings: BindingMap, + initialBindings: KeyBindingLoopMap, options?: { delay?: number; supportInputElements?: boolean; @@ -333,17 +336,12 @@ class InputMouseButton { mouse: InputMouse; name: MouseButtonString; which: MouseButtonWhich; - id: string | null | undefined; + id: string; down: boolean = false; drag: boolean = false; moveDelta: number = 0; - constructor( - name: MouseButtonString, - which: MouseButtonWhich, - mouse: InputMouse, - id: string | null | undefined, - ) { + constructor(name: MouseButtonString, which: MouseButtonWhich, mouse: InputMouse, id: string) { this.name = name; this.which = which; this.mouse = mouse; @@ -399,7 +397,7 @@ export class InputMouse { emitter: Emitter; targetId: string; hammerManager: typeof Hammer; - id: string | null | undefined; + id: string; leftMouseButton: InputMouseButton; middleMouseButton: InputMouseButton; rightMouseButton: InputMouseButton; @@ -416,8 +414,8 @@ export class InputMouse { constructor( targetId: string, - initialBindings: BindingMap = {}, - id: string | null | undefined = null, + initialBindings: MouseBindingMap, + id: string, ignoreScrollingWhileDragging: boolean = false, ) { this.emitter = createNanoEvents(); @@ -640,8 +638,8 @@ export class InputMouse { modifier = "shift"; } else if (event.altKey) { modifier = "alt"; - } else if (event.ctrlKey) { - modifier = "ctrl"; + } else if (event.ctrlKey || event.metaKey) { + modifier = "ctrlOrMeta"; } this.emitter.emit("scroll", delta, modifier); diff --git a/frontend/javascripts/libs/react_hooks.ts b/frontend/javascripts/libs/react_hooks.ts index 18db4a8ea07..12ed35e27cf 100644 --- a/frontend/javascripts/libs/react_hooks.ts +++ b/frontend/javascripts/libs/react_hooks.ts @@ -23,13 +23,13 @@ const extractModifierState = (event: WindowEvent // @ts-ignore Shift: event.shiftKey, // @ts-ignore - Alt: event.altKey, + Alt: event.altKey, // This is the option key ⌥ on MacOS // @ts-ignore - Control: event.ctrlKey, + ControlOrMeta: event.ctrlKey || event.metaKey, }); // Adapted from: https://gist.github.com/gragland/b61b8f46114edbcf2a9e4bd5eb9f47f5 -export function useKeyPress(targetKey: "Shift" | "Alt" | "Control") { +export function useKeyPress(targetKey: "Shift" | "Alt" | "ControlOrMeta") { // State for keeping track of whether key is pressed const [keyPressed, setKeyPressed] = useState(false); diff --git a/frontend/javascripts/oxalis/constants.ts b/frontend/javascripts/oxalis/constants.ts index 311569236b1..7f62ad3177e 100644 --- a/frontend/javascripts/oxalis/constants.ts +++ b/frontend/javascripts/oxalis/constants.ts @@ -75,7 +75,7 @@ export const ArbitraryViewport = "arbitraryViewport"; export const ArbitraryViews = { arbitraryViewport: "arbitraryViewport", TDView: "TDView", -}; +} as const; export const ArbitraryViewsToName = { arbitraryViewport: "Arbitrary View", TDView: "3D", @@ -265,6 +265,12 @@ export type TreeType = keyof typeof TreeTypeEnum; export const NODE_ID_REF_REGEX = /#([0-9]+)/g; export const POSITION_REF_REGEX = /#\(([0-9]+,[0-9]+,[0-9]+)\)/g; const VIEWPORT_WIDTH = 376; + +// ARBITRARY_CAM_DISTANCE has to be calculated such that with cam +// angle 45°, the plane of width Constants.VIEWPORT_WIDTH fits exactly in the +// viewport. +export const ARBITRARY_CAM_DISTANCE = VIEWPORT_WIDTH / 2 / Math.tan(((Math.PI / 180) * 45) / 2); + export const ensureSmallerEdge = false; export const Unicode = { ThinSpace: "\u202f", @@ -382,3 +388,20 @@ export const IdentityTransform = { affineMatrixInv: Identity4x4, } as const; export const EMPTY_OBJECT = {} as const; + +const isMac = (() => { + try { + // Even though navigator.platform¹ is deprecated, this still + // seems to be the best mechanism to find out whether the machine is + // a Mac. At some point, NavigatorUAData² might be a feasible alternative. + // + // ¹ https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform + // ² https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/platform + return navigator.platform.toUpperCase().indexOf("MAC") >= 0; + } catch { + return false; + } +})(); + +export const AltOrOptionKey = isMac ? "⌥" : "Alt"; +export const CtrlOrCmdKey = isMac ? "Cmd" : "Ctrl"; diff --git a/frontend/javascripts/oxalis/controller/combinations/move_handlers.ts b/frontend/javascripts/oxalis/controller/combinations/move_handlers.ts index bf918ab9f66..e3ea5e9af05 100644 --- a/frontend/javascripts/oxalis/controller/combinations/move_handlers.ts +++ b/frontend/javascripts/oxalis/controller/combinations/move_handlers.ts @@ -68,7 +68,7 @@ export function moveWhenAltIsPressed(delta: Point2, position: Point2, _id: any, // alt + scroll won't result in the correct zoomToMouse behavior. setMousePosition(position); - if (event.altKey && !event.shiftKey && !event.ctrlKey) { + if (event.altKey && !event.shiftKey && !(event.ctrlKey || event.metaKey)) { handleMovePlane(delta); } } diff --git a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts index aa4047ee4c8..f631bcbe70c 100644 --- a/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts +++ b/frontend/javascripts/oxalis/controller/combinations/skeleton_handlers.ts @@ -5,6 +5,7 @@ import type { Point2, Vector3, ShowContextMenuFunction, + Viewport, } from "oxalis/constants"; import { OrthoViews } from "oxalis/constants"; import { V3 } from "libs/mjs"; @@ -51,6 +52,7 @@ import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import Dimensions from "oxalis/model/dimensions"; import { getClosestHoveredBoundingBox } from "oxalis/controller/combinations/bounding_box_handlers"; import { getEnabledColorLayers } from "oxalis/model/accessors/dataset_accessor"; +import ArbitraryView from "oxalis/view/arbitrary_view"; const OrthoViewToNumber: OrthoViewMap = { [OrthoViews.PLANE_XY]: 0, [OrthoViews.PLANE_YZ]: 1, @@ -58,12 +60,12 @@ const OrthoViewToNumber: OrthoViewMap = { [OrthoViews.TDView]: 3, }; export function handleMergeTrees( - planeView: PlaneView, + view: PlaneView | ArbitraryView, position: Point2, - plane: OrthoView, + plane: Viewport, isTouch: boolean, ) { - const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch); + const nodeId = maybeGetNodeIdFromPosition(view, position, plane, isTouch); const skeletonTracing = enforceSkeletonTracing(Store.getState().tracing); // otherwise we have hit the background and do nothing @@ -74,12 +76,12 @@ export function handleMergeTrees( } } export function handleDeleteEdge( - planeView: PlaneView, + view: PlaneView | ArbitraryView, position: Point2, - plane: OrthoView, + plane: Viewport, isTouch: boolean, ) { - const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch); + const nodeId = maybeGetNodeIdFromPosition(view, position, plane, isTouch); const skeletonTracing = enforceSkeletonTracing(Store.getState().tracing); // otherwise we have hit the background and do nothing @@ -90,12 +92,12 @@ export function handleDeleteEdge( } } export function handleSelectNode( - planeView: PlaneView, + view: PlaneView | ArbitraryView, position: Point2, - plane: OrthoView, + plane: Viewport, isTouch: boolean, ): boolean { - const nodeId = maybeGetNodeIdFromPosition(planeView, position, plane, isTouch); + const nodeId = maybeGetNodeIdFromPosition(view, position, plane, isTouch); // otherwise we have hit the background and do nothing if (nodeId != null && nodeId > 0) { @@ -105,7 +107,7 @@ export function handleSelectNode( return false; } -export function handleCreateNode(_planeView: PlaneView, position: Point2, ctrlPressed: boolean) { +export function handleCreateNode(position: Point2, ctrlPressed: boolean) { const state = Store.getState(); if (isMagRestrictionViolated(state)) { @@ -319,9 +321,9 @@ export function moveAlongDirection(reverse: boolean = false): void { api.tracing.centerPositionAnimated(newPosition, false); } export function maybeGetNodeIdFromPosition( - planeView: PlaneView, + planeView: PlaneView | ArbitraryView, position: Point2, - plane: OrthoView, + plane: Viewport, isTouch: boolean, ): number | null | undefined { const SceneController = getSceneController(); @@ -344,7 +346,8 @@ export function maybeGetNodeIdFromPosition( const pickingNode = skeleton.startPicking(isTouch); const pickingScene = new THREE.Scene(); pickingScene.add(pickingNode); - const camera = planeView.getCameras()[plane]; + const camera = planeView.getCameraForPlane(plane); + let { width, height } = getInputCatcherRect(Store.getState(), plane); width = Math.round(width); height = Math.round(height); diff --git a/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts b/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts index 0d6db81a6c1..db07d19041d 100644 --- a/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts +++ b/frontend/javascripts/oxalis/controller/combinations/tool_controls.ts @@ -6,6 +6,7 @@ import type { ShowContextMenuFunction, AnnotationTool, Vector3, + Viewport, } from "oxalis/constants"; import { OrthoViews, ContourModeEnum, AnnotationToolEnum } from "oxalis/constants"; import { @@ -57,6 +58,7 @@ import { setLastMeasuredPositionAction, setIsMeasuringAction, } from "oxalis/model/actions/ui_actions"; +import ArbitraryView from "oxalis/view/arbitrary_view"; export type ActionDescriptor = { leftClick?: string; @@ -98,7 +100,7 @@ export class MoveTool { } case "alt": - case "ctrl": { + case "ctrlOrMeta": { MoveHandlers.zoomPlanes(Utils.clamp(-1, delta, 1), true); break; } @@ -184,13 +186,17 @@ export class MoveTool { _activeTool: AnnotationTool, useLegacyBindings: boolean, shiftKey: boolean, - _ctrlKey: boolean, - _altKey: boolean, + _ctrlOrMetaKey: boolean, + altKey: boolean, ): ActionDescriptor { // In legacy mode, don't display a hint for - // left click as it would be equal to left drag + // left click as it would be equal to left drag. + // We also don't show a hint when the alt key was pressed, + // as this mostly happens when the user presses alt in another tool + // to move around while moving the mouse. In that case, clicking won't + // select anything. const leftClickInfo = - useLegacyBindings && !shiftKey + (useLegacyBindings && !shiftKey) || altKey ? {} : { leftClick: "Select Node", @@ -224,7 +230,7 @@ export class SkeletonTool { showNodeContextMenuAt, ); } else { - SkeletonHandlers.handleCreateNode(planeView, position, event.ctrlKey); + SkeletonHandlers.handleCreateNode(position, event.ctrlKey || event.metaKey); } }; @@ -264,7 +270,7 @@ export class SkeletonTool { if ( tracing.skeleton != null && - (draggingNodeId != null || (useLegacyBindings && event.ctrlKey)) + (draggingNodeId != null || (useLegacyBindings && (event.ctrlKey || event.metaKey))) ) { didDragNode = true; SkeletonHandlers.moveNode(delta.x, delta.y, draggingNodeId, true); @@ -273,26 +279,15 @@ export class SkeletonTool { } }, leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { - const { useLegacyBindings } = Store.getState().userConfiguration; - - if (useLegacyBindings) { - this.onLegacyLeftClick( - planeView, - pos, - event.shiftKey, - event.altKey, - event.ctrlKey, - plane, - isTouch, - ); - return; - } - - const didSelectNode = SkeletonHandlers.handleSelectNode(planeView, pos, plane, isTouch); - - if (!didSelectNode) { - SkeletonHandlers.handleCreateNode(planeView, pos, event.ctrlKey); - } + this.onLeftClick( + planeView, + pos, + event.shiftKey, + event.altKey, + event.ctrlKey || event.metaKey, + plane, + isTouch, + ); }, rightClick: (position: Point2, plane: OrthoView, event: MouseEvent, isTouch: boolean) => { const { useLegacyBindings } = Store.getState().userConfiguration; @@ -314,14 +309,15 @@ export class SkeletonTool { }; } - static onLegacyLeftClick( - planeView: PlaneView, + static onLeftClick( + planeView: PlaneView | ArbitraryView, position: Point2, shiftPressed: boolean, altPressed: boolean, ctrlPressed: boolean, - plane: OrthoView, + plane: Viewport, isTouch: boolean, + allowNodeCreation: boolean = true, ): void { const { useLegacyBindings } = Store.getState().userConfiguration; @@ -329,10 +325,20 @@ export class SkeletonTool { // (At least, in the XY/XZ/YZ viewports). if (shiftPressed && altPressed) { SkeletonHandlers.handleMergeTrees(planeView, position, plane, isTouch); + return; } else if (shiftPressed && ctrlPressed) { SkeletonHandlers.handleDeleteEdge(planeView, position, plane, isTouch); - } else if (shiftPressed || !useLegacyBindings) { - SkeletonHandlers.handleSelectNode(planeView, position, plane, isTouch); + return; + } + + let didSelectNode; + if (shiftPressed || !useLegacyBindings) { + didSelectNode = SkeletonHandlers.handleSelectNode(planeView, position, plane, isTouch); + } + + if (allowNodeCreation && !didSelectNode && !useLegacyBindings && !shiftPressed) { + // Will only have an effect, when not in 3D viewport + SkeletonHandlers.handleCreateNode(position, ctrlPressed); } } @@ -340,16 +346,34 @@ export class SkeletonTool { _activeTool: AnnotationTool, useLegacyBindings: boolean, shiftKey: boolean, - _ctrlKey: boolean, - _altKey: boolean, + ctrlOrMetaKey: boolean, + altKey: boolean, ): ActionDescriptor { // In legacy mode, don't display a hint for // left click as it would be equal to left drag - const leftClickInfo = useLegacyBindings - ? {} - : { - leftClick: "Place/Select Node", - }; + let leftClickInfo = {}; + if (shiftKey && altKey) { + leftClickInfo = { + leftClick: "Create edge between nodes", + }; + } else if (shiftKey && ctrlOrMetaKey) { + leftClickInfo = { + leftClick: "Delete edge between nodes", + }; + } else if (shiftKey) { + leftClickInfo = { + leftClick: "Select node", + }; + } else if (!useLegacyBindings && ctrlOrMetaKey && !shiftKey && !altKey) { + leftClickInfo = { + leftClick: "Place Node without Activating", + }; + } else if (!useLegacyBindings && !ctrlOrMetaKey && !shiftKey && !altKey) { + leftClickInfo = { + leftClick: "Place/Select Node", + }; + } + return { ...leftClickInfo, leftDrag: "Move", @@ -370,12 +394,13 @@ export class DrawTool { VolumeHandlers.handleMoveForDrawOrErase(pos); }, leftMouseDown: (pos: Point2, plane: OrthoView, event: MouseEvent) => { - if (event.shiftKey && !event.ctrlKey) { + const ctrlOrMetaPressed = event.ctrlKey || event.metaKey; + if (event.shiftKey && !ctrlOrMetaPressed) { // Should select cell. Do nothing, since case is covered by leftClick. return; } - if (event.ctrlKey && event.shiftKey) { + if (ctrlOrMetaPressed && event.shiftKey) { VolumeHandlers.handleEraseStart(pos, plane); return; } @@ -421,8 +446,9 @@ export class DrawTool { VolumeHandlers.handleEndForDrawOrErase(); }, leftClick: (pos: Point2, _plane: OrthoView, event: MouseEvent) => { - const shouldPickCell = event.shiftKey && !event.ctrlKey; - const shouldErase = event.shiftKey && event.ctrlKey; + const ctrlOrMetaPressed = event.ctrlKey || event.metaKey; + const shouldPickCell = event.shiftKey && !ctrlOrMetaPressed; + const shouldErase = event.shiftKey && ctrlOrMetaPressed; if (shouldPickCell) { VolumeHandlers.handlePickCell(pos); @@ -457,7 +483,7 @@ export class DrawTool { activeTool: AnnotationTool, useLegacyBindings: boolean, _shiftKey: boolean, - _ctrlKey: boolean, + _ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { let rightClick; @@ -512,7 +538,7 @@ export class EraseTool { activeTool: AnnotationTool, _useLegacyBindings: boolean, _shiftKey: boolean, - _ctrlKey: boolean, + _ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { return { @@ -536,7 +562,7 @@ export class PickCellTool { _activeTool: AnnotationTool, _useLegacyBindings: boolean, _shiftKey: boolean, - _ctrlKey: boolean, + _ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { return { @@ -551,7 +577,7 @@ export class FillCellTool { static getPlaneMouseControls(_planeId: OrthoView): any { return { leftClick: (pos: Point2, plane: OrthoView, event: MouseEvent) => { - const shouldPickCell = event.shiftKey && !event.ctrlKey; + const shouldPickCell = event.shiftKey && !(event.ctrlKey || event.metaKey); if (shouldPickCell) { VolumeHandlers.handlePickCell(pos); @@ -566,7 +592,7 @@ export class FillCellTool { _activeTool: AnnotationTool, _useLegacyBindings: boolean, _shiftKey: boolean, - _ctrlKey: boolean, + _ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { return { @@ -645,7 +671,7 @@ export class BoundingBoxTool { _activeTool: AnnotationTool, _useLegacyBindings: boolean, _shiftKey: boolean, - _ctrlKey: boolean, + _ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { return { @@ -773,7 +799,7 @@ export class QuickSelectTool { _activeTool: AnnotationTool, _useLegacyBindings: boolean, shiftKey: boolean, - _ctrlKey: boolean, + _ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { return { @@ -896,7 +922,7 @@ export class LineMeasurementTool { _activeTool: AnnotationTool, _useLegacyBindings: boolean, _shiftKey: boolean, - _ctrlKey: boolean, + _ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { return { @@ -974,7 +1000,7 @@ export class AreaMeasurementTool { _activeTool: AnnotationTool, _useLegacyBindings: boolean, _shiftKey: boolean, - _ctrlKey: boolean, + _ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { return { @@ -1019,7 +1045,7 @@ export class ProofreadTool { if (event.shiftKey) { Store.dispatch(proofreadMerge(globalPosition)); - } else if (event.ctrlKey) { + } else if (event.ctrlKey || event.metaKey) { Store.dispatch(minCutAgglomerateWithPositionAction(globalPosition)); } else { Store.dispatch( @@ -1033,14 +1059,14 @@ export class ProofreadTool { _activeTool: AnnotationTool, _useLegacyBindings: boolean, shiftKey: boolean, - ctrlKey: boolean, + ctrlOrMetaKey: boolean, _altKey: boolean, ): ActionDescriptor { let leftClick = "Select Segment to Proofread"; if (shiftKey) { leftClick = "Merge with active Segment"; - } else if (ctrlKey) { + } else if (ctrlOrMetaKey) { leftClick = "Split from active Segment"; } diff --git a/frontend/javascripts/oxalis/controller/td_controller.tsx b/frontend/javascripts/oxalis/controller/td_controller.tsx index c0fc0091102..2cee9c69d05 100644 --- a/frontend/javascripts/oxalis/controller/td_controller.tsx +++ b/frontend/javascripts/oxalis/controller/td_controller.tsx @@ -68,12 +68,12 @@ function getTDViewMouseControlsSkeleton(planeView: PlaneView): Record activeTool === AnnotationToolEnum.PROOFREAD ? ProofreadTool.onLeftClick(planeView, pos, plane, event, isTouch) - : SkeletonTool.onLegacyLeftClick( + : SkeletonTool.onLeftClick( planeView, pos, event.shiftKey, event.altKey, - event.ctrlKey, + event.ctrlKey || event.metaKey, OrthoViews.TDView, isTouch, ), @@ -220,7 +220,8 @@ class TDController extends React.PureComponent { return; } - if (!event.shiftKey && !event.ctrlKey) { + const ctrlOrMetaPressed = event.ctrlKey || event.metaKey; + if (!event.shiftKey && !ctrlOrMetaPressed) { // No modifiers were pressed. No mesh related action is necessary. return; } @@ -236,7 +237,7 @@ class TDController extends React.PureComponent { if (event.shiftKey) { Store.dispatch(setPositionAction(unscaledPosition)); - } else if (event.ctrlKey) { + } else if (ctrlOrMetaPressed) { const storeState = Store.getState(); const { hoveredSegmentId } = storeState.temporaryConfiguration; const segmentationLayer = getVisibleSegmentationLayer(storeState); diff --git a/frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.tsx b/frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.tsx index 17d8240caeb..ab6053580fa 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.tsx +++ b/frontend/javascripts/oxalis/controller/viewmodes/arbitrary_controller.tsx @@ -20,6 +20,7 @@ import { requestDeleteBranchPointAction, toggleAllTreesAction, toggleInactiveTreesAction, + createTreeAction, } from "oxalis/model/actions/skeletontracing_actions"; import { setFlightmodeRecordingAction, @@ -40,11 +41,12 @@ import TDController from "oxalis/controller/td_controller"; import Toast from "libs/toast"; import * as Utils from "libs/utils"; import { api } from "oxalis/singletons"; -import type { ViewMode, Point2, Vector3 } from "oxalis/constants"; +import type { ViewMode, Point2, Vector3, Viewport } from "oxalis/constants"; import constants, { ArbitraryViewport } from "oxalis/constants"; import getSceneController from "oxalis/controller/scene_controller_provider"; import messages from "messages"; import { downloadScreenshot } from "oxalis/view/rendering_utils"; +import { SkeletonTool } from "../combinations/tool_controls"; const arbitraryViewportId = "inputcatcher_arbitraryViewport"; type Props = { @@ -90,34 +92,53 @@ class ArbitraryController extends React.PureComponent { initMouse(): void { Utils.waitForElementWithId(arbitraryViewportId).then(() => { - this.input.mouseController = new InputMouse(arbitraryViewportId, { - leftDownMove: (delta: Point2) => { - if (this.props.viewMode === constants.MODE_ARBITRARY) { - Store.dispatch( - yawFlycamAction(delta.x * Store.getState().userConfiguration.mouseRotateValue, true), + this.input.mouseController = new InputMouse( + arbitraryViewportId, + { + leftClick: (pos: Point2, viewport: string, event: MouseEvent, isTouch: boolean) => { + SkeletonTool.onLeftClick( + this.arbitraryView, + pos, + event.shiftKey, + event.altKey, + event.ctrlKey || event.metaKey, + viewport as Viewport, + isTouch, + false, ); - Store.dispatch( - pitchFlycamAction( - delta.y * -1 * Store.getState().userConfiguration.mouseRotateValue, - true, - ), - ); - } else if (this.props.viewMode === constants.MODE_ARBITRARY_PLANE) { - const [scaleX, scaleY] = getViewportScale(Store.getState(), ArbitraryViewport); - const fx = Store.getState().flycam.zoomStep / scaleX; - const fy = Store.getState().flycam.zoomStep / scaleY; - Store.dispatch(moveFlycamAction([delta.x * fx, delta.y * fy, 0])); - } + }, + leftDownMove: (delta: Point2) => { + if (this.props.viewMode === constants.MODE_ARBITRARY) { + Store.dispatch( + yawFlycamAction( + delta.x * Store.getState().userConfiguration.mouseRotateValue, + true, + ), + ); + Store.dispatch( + pitchFlycamAction( + delta.y * -1 * Store.getState().userConfiguration.mouseRotateValue, + true, + ), + ); + } else if (this.props.viewMode === constants.MODE_ARBITRARY_PLANE) { + const [scaleX, scaleY] = getViewportScale(Store.getState(), ArbitraryViewport); + const fx = Store.getState().flycam.zoomStep / scaleX; + const fy = Store.getState().flycam.zoomStep / scaleY; + Store.dispatch(moveFlycamAction([delta.x * fx, delta.y * fy, 0])); + } + }, + scroll: this.scroll, + pinch: (delta: number) => { + if (delta < 0) { + Store.dispatch(zoomOutAction()); + } else { + Store.dispatch(zoomInAction()); + } + }, }, - scroll: this.scroll, - pinch: (delta: number) => { - if (delta < 0) { - Store.dispatch(zoomOutAction()); - } else { - Store.dispatch(zoomInAction()); - } - }, - }); + ArbitraryViewport, + ); }); } @@ -196,6 +217,16 @@ class ArbitraryController extends React.PureComponent { "2": () => { Store.dispatch(toggleInactiveTreesAction()); }, + // Delete active node + delete: () => { + Store.dispatch(deleteNodeAsUserAction(Store.getState())); + }, + backspace: () => { + Store.dispatch(deleteNodeAsUserAction(Store.getState())); + }, + c: () => { + Store.dispatch(createTreeAction()); + }, // Branches b: () => this.pushBranch(), j: () => { diff --git a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.tsx b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.tsx index 5ed7dcd078c..2a089acf11d 100644 --- a/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.tsx +++ b/frontend/javascripts/oxalis/controller/viewmodes/plane_controller.tsx @@ -11,7 +11,7 @@ import { toggleInactiveTreesAction, } from "oxalis/model/actions/skeletontracing_actions"; import { addUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; -import { InputKeyboard, InputKeyboardNoLoop, InputMouse } from "libs/input"; +import { InputKeyboard, InputKeyboardNoLoop, InputMouse, MouseBindingMap } from "libs/input"; import { document } from "libs/window"; import { getPosition, @@ -303,7 +303,7 @@ class PlaneController extends React.PureComponent { }); } - getPlaneMouseControls(planeId: OrthoView): Record { + getPlaneMouseControls(planeId: OrthoView): MouseBindingMap { const moveControls = MoveTool.getMouseControls( planeId, this.planeView, @@ -353,7 +353,7 @@ class PlaneController extends React.PureComponent { Object.keys(areaMeasurementControls), ); - const controls: Record = {}; + const controls: MouseBindingMap = {}; for (const controlKey of allControlKeys) { controls[controlKey] = this.createToolDependentMouseHandler({ diff --git a/frontend/javascripts/oxalis/geometries/cube.ts b/frontend/javascripts/oxalis/geometries/cube.ts index 12675816ec2..75c7e9e4c38 100644 --- a/frontend/javascripts/oxalis/geometries/cube.ts +++ b/frontend/javascripts/oxalis/geometries/cube.ts @@ -27,7 +27,7 @@ class Cube { cube: THREE.Line; min: Vector3; max: Vector3; - showCrossSections: boolean; + readonly showCrossSections: boolean; initialized: boolean; visible: boolean; lineWidth: number; @@ -57,10 +57,12 @@ class Cube { this.setCorners(this.min, this.max); } - listenToStoreProperty( - (state) => getPosition(state.flycam), - (position) => this.updatePositionForCrossSections(position), - ); + if (this.showCrossSections) { + listenToStoreProperty( + (state) => getPosition(state.flycam), + (position) => this.updatePositionForCrossSections(position), + ); + } } getLineMaterial() { diff --git a/frontend/javascripts/oxalis/geometries/materials/node_shader.ts b/frontend/javascripts/oxalis/geometries/materials/node_shader.ts index b990f8d42d7..6fe14f384b3 100644 --- a/frontend/javascripts/oxalis/geometries/materials/node_shader.ts +++ b/frontend/javascripts/oxalis/geometries/materials/node_shader.ts @@ -324,6 +324,9 @@ void main() { ? v_innerPointSize + 25.0 : v_innerPointSize * 1.5; gl_PointSize = v_outerPointSize; + + // Shift hue to further highlight active node in arbitrary mode. + color = shiftHue(color, isOrthogonalMode ? 0. : 0.15); } float isBranchpoint = diff --git a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts index f108cde0173..9c700dd91e6 100644 --- a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts @@ -376,7 +376,7 @@ export function adaptActiveToolToShortcuts( if (activeTool === AnnotationToolEnum.SKELETON) { // The "skeleton" tool is not changed right now (since actions such as moving a node // don't have a dedicated tool). The only exception is "Alt" which switches to the move tool. - if (isAltPressed) { + if (isAltPressed && !isControlPressed && !isShiftPressed) { return AnnotationToolEnum.MOVE; } diff --git a/frontend/javascripts/oxalis/model/helpers/shader_editor.ts b/frontend/javascripts/oxalis/model/helpers/shader_editor.ts index d6bb49f03c8..c1db5b9c0f5 100644 --- a/frontend/javascripts/oxalis/model/helpers/shader_editor.ts +++ b/frontend/javascripts/oxalis/model/helpers/shader_editor.ts @@ -44,7 +44,7 @@ window._setupShaderEditor = (identifier, _shaderType) => { `, ); input.addEventListener("keydown", (evt) => { - if ((evt.keyCode === 10 || evt.keyCode === 13) && evt.ctrlKey) { + if ((evt.keyCode === 10 || evt.keyCode === 13) && (evt.ctrlKey || event.metaKey)) { evt.preventDefault(); overrideShader(); } diff --git a/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.ts b/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.ts index 0e54637173a..84f4e720c6e 100644 --- a/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/annotation_tool_saga.ts @@ -4,7 +4,6 @@ import { take, call, put } from "typed-redux-saga"; import { type SetToolAction, type CycleToolAction, - type EscapeAction, hideMeasurementTooltipAction, setIsMeasuringAction, } from "oxalis/model/actions/ui_actions"; @@ -39,7 +38,7 @@ export function* watchToolDeselection(): Saga { export function* watchToolReset(): Saga { while (true) { - (yield* take("ESCAPE") as any) as EscapeAction; + yield* take("ESCAPE"); const activeTool = yield* select((state) => state.uiInformation.activeTool); if (MeasurementTools.indexOf(activeTool) >= 0) { const sceneController = yield* call(() => getSceneController()); diff --git a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx index 595f49da2f3..4e44837f11b 100644 --- a/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx @@ -98,30 +98,30 @@ const imgStyleForSpaceyIcons = { function getSkeletonToolHint( activeTool: AnnotationTool, isShiftPressed: boolean, - isControlPressed: boolean, + isControlOrMetaPressed: boolean, isAltPressed: boolean, ): string | null | undefined { if (activeTool !== AnnotationToolEnum.SKELETON) { return null; } - if (!isShiftPressed && !isControlPressed && !isAltPressed) { + if (!isShiftPressed && !isControlOrMetaPressed && !isAltPressed) { return null; } - if (isShiftPressed && !isControlPressed && !isAltPressed) { + if (isShiftPressed && !isControlOrMetaPressed && !isAltPressed) { return "Click to select a node. Right-click to open a contextmenu."; } - if (!isShiftPressed && isControlPressed && !isAltPressed) { + if (!isShiftPressed && isControlOrMetaPressed && !isAltPressed) { return "Drag to move the selected node. Right-click to create a new node without selecting it."; } - if (isShiftPressed && !isControlPressed && isAltPressed) { + if (isShiftPressed && !isControlOrMetaPressed && isAltPressed) { return "Click on a node in another tree to merge the two trees."; } - if (isShiftPressed && isControlPressed && !isAltPressed) { + if (isShiftPressed && isControlOrMetaPressed && !isAltPressed) { return "Click on a node to delete the edge to the currently active node."; } @@ -242,18 +242,18 @@ function ToolRadioButton({ } function OverwriteModeSwitch({ - isControlPressed, + isControlOrMetaPressed, isShiftPressed, visible, }: { - isControlPressed: boolean; + isControlOrMetaPressed: boolean; isShiftPressed: boolean; visible: boolean; }) { // Only CTRL should modify the overwrite mode. CTRL + Shift can be used to switch to the // erase tool, which should not affect the default overwrite mode. const overwriteMode = useSelector((state: OxalisState) => state.userConfiguration.overwriteMode); - const previousIsControlPressed = usePrevious(isControlPressed); + const previousIsControlOrMetaPressed = usePrevious(isControlOrMetaPressed); const previousIsShiftPressed = usePrevious(isShiftPressed); // biome-ignore lint/correctness/useExhaustiveDependencies: overwriteMode does not need to be a dependency. useEffect(() => { @@ -270,18 +270,25 @@ function OverwriteModeSwitch({ // separately in the store. However, this solution works, too. const needsModeToggle = (!isShiftPressed && - isControlPressed && - previousIsControlPressed === previousIsShiftPressed) || - (isShiftPressed === isControlPressed && !previousIsShiftPressed && previousIsControlPressed); + isControlOrMetaPressed && + previousIsControlOrMetaPressed === previousIsShiftPressed) || + (isShiftPressed === isControlOrMetaPressed && + !previousIsShiftPressed && + previousIsControlOrMetaPressed); if (needsModeToggle) { Store.dispatch(updateUserSettingAction("overwriteMode", toggleOverwriteMode(overwriteMode))); } - }, [isControlPressed, isShiftPressed, previousIsControlPressed, previousIsShiftPressed]); + }, [ + isControlOrMetaPressed, + isShiftPressed, + previousIsControlOrMetaPressed, + previousIsShiftPressed, + ]); if (!visible) { // This component's hooks should still be active, even when the component is invisible. - // Otherwise, the toggling of the overwrite mode via "CTRL" wouldn't work consistently + // Otherwise, the toggling of the overwrite mode via "Ctrl" wouldn't work consistently // when being combined with other modifiers, which hide the component. return null; } @@ -909,17 +916,17 @@ export default function ToolbarView() { }, [activeTool, disabledInfoForCurrentTool, lastForcefulDisabledTool]); const isShiftPressed = useKeyPress("Shift"); - const isControlPressed = useKeyPress("Control"); + const isControlOrMetaPressed = useKeyPress("ControlOrMeta"); const isAltPressed = useKeyPress("Alt"); const adaptedActiveTool = adaptActiveToolToShortcuts( activeTool, isShiftPressed, - isControlPressed, + isControlOrMetaPressed, isAltPressed, ); const skeletonToolHint = hasSkeleton && useLegacyBindings - ? getSkeletonToolHint(activeTool, isShiftPressed, isControlPressed, isAltPressed) + ? getSkeletonToolHint(activeTool, isShiftPressed, isControlOrMetaPressed, isAltPressed) : null; const previousSkeletonToolHint = usePrevious(skeletonToolHint); @@ -1185,7 +1192,7 @@ export default function ToolbarView() { hasSkeleton={hasSkeleton} adaptedActiveTool={adaptedActiveTool} hasVolume={hasVolume} - isControlPressed={isControlPressed} + isControlOrMetaPressed={isControlOrMetaPressed} isShiftPressed={isShiftPressed} /> @@ -1196,13 +1203,13 @@ function ToolSpecificSettings({ hasSkeleton, adaptedActiveTool, hasVolume, - isControlPressed, + isControlOrMetaPressed, isShiftPressed, }: { hasSkeleton: boolean; adaptedActiveTool: AnnotationTool; hasVolume: boolean; - isControlPressed: boolean; + isControlOrMetaPressed: boolean; isShiftPressed: boolean; }) { const showCreateTreeButton = hasSkeleton && adaptedActiveTool === AnnotationToolEnum.SKELETON; @@ -1268,7 +1275,7 @@ function ToolSpecificSettings({ ) : null} diff --git a/frontend/javascripts/oxalis/view/arbitrary_view.ts b/frontend/javascripts/oxalis/view/arbitrary_view.ts index 9598861b900..64d48594da0 100644 --- a/frontend/javascripts/oxalis/view/arbitrary_view.ts +++ b/frontend/javascripts/oxalis/view/arbitrary_view.ts @@ -9,8 +9,8 @@ import { import { getInputCatcherRect } from "oxalis/model/accessors/view_mode_accessor"; import { getZoomedMatrix } from "oxalis/model/accessors/flycam_accessor"; import type ArbitraryPlane from "oxalis/geometries/arbitrary_plane"; -import type { OrthoViewMap } from "oxalis/constants"; -import Constants, { ArbitraryViewport, OrthoViews } from "oxalis/constants"; +import type { OrthoViewMap, Viewport } from "oxalis/constants"; +import Constants, { ARBITRARY_CAM_DISTANCE, ArbitraryViewport, OrthoViews } from "oxalis/constants"; import Store from "oxalis/store"; import app from "app"; import getSceneController from "oxalis/controller/scene_controller_provider"; @@ -32,7 +32,6 @@ class ArbitraryView { additionalInfo: string = ""; isRunning: boolean = false; animationRequestId: number | null | undefined = null; - camDistance: number; // @ts-expect-error ts-migrate(2322) FIXME: Type 'null' is not assignable to type 'Perspective... Remove this comment to see the full error message camera: THREE.PerspectiveCamera = null; // @ts-expect-error ts-migrate(2322) FIXME: Type 'null' is not assignable to type 'Orthographi... Remove this comment to see the full error message @@ -48,10 +47,6 @@ class ArbitraryView { this.setClippingDistance = this.setClippingDistanceImpl.bind(this); const { scene } = getSceneController(); - // camDistance has to be calculated such that with cam - // angle 45°, the plane of width Constants.VIEWPORT_WIDTH fits exactly in the - // viewport. - this.camDistance = Constants.VIEWPORT_WIDTH / 2 / Math.tan(((Math.PI / 180) * 45) / 2); // Initialize main THREE.js components this.camera = new THREE.PerspectiveCamera(45, 1, 50, 1000); // This name can be used to retrieve the camera from the scene @@ -63,17 +58,14 @@ class ArbitraryView { tdCamera.up = new THREE.Vector3(0, 0, -1); tdCamera.matrixAutoUpdate = true; this.tdCamera = tdCamera; - const dummyCamera = new THREE.PerspectiveCamera(45, 1, 50, 1000); + const dummyCamera = new THREE.OrthographicCamera(45, 1, 50, 1000); this.cameras = { TDView: tdCamera, - // @ts-expect-error ts-migrate(2739) FIXME: Type 'PerspectiveCamera' is missing the following ... Remove this comment to see the full error message PLANE_XY: dummyCamera, - // @ts-expect-error ts-migrate(2322) FIXME: Type 'PerspectiveCamera' is not assignable to type... Remove this comment to see the full error message PLANE_YZ: dummyCamera, - // @ts-expect-error ts-migrate(2322) FIXME: Type 'PerspectiveCamera' is not assignable to type... Remove this comment to see the full error message PLANE_XZ: dummyCamera, }; - this.cameraPosition = [0, 0, this.camDistance]; + this.cameraPosition = [0, 0, ARBITRARY_CAM_DISTANCE]; this.needsRerender = true; } @@ -137,12 +129,15 @@ class ArbitraryView { } animateImpl(): void { - this.animationRequestId = null; - if (!this.isRunning) { return; } + this.renderFunction(); + this.animationRequestId = window.requestAnimationFrame(this.animate); + } + renderFunction() { + this.animationRequestId = null; TWEEN.update(); if (this.needsRerender) { @@ -158,23 +153,12 @@ class ArbitraryView { } const m = getZoomedMatrix(Store.getState().flycam); + // biome-ignore format: don't format array camera.matrix.set( - m[0], - m[4], - m[8], - m[12], - m[1], - m[5], - m[9], - m[13], - m[2], - m[6], - m[10], - m[14], - m[3], - m[7], - m[11], - m[15], + m[0], m[4], m[8], m[12], + m[1], m[5], m[9], m[13], + m[2], m[6], m[10], m[14], + m[3], m[7], m[11], m[15], ); camera.matrix.multiply(new THREE.Matrix4().makeRotationY(Math.PI)); // @ts-expect-error ts-migrate(2556) FIXME: Expected 3 arguments, but got 0 or more. @@ -209,8 +193,6 @@ class ArbitraryView { this.needsRerender = false; } - - this.animationRequestId = window.requestAnimationFrame(this.animate); } draw(): void { @@ -288,13 +270,17 @@ class ArbitraryView { resizeThrottled = _.throttle(this.resizeImpl, Constants.RESIZE_THROTTLE_TIME); setClippingDistanceImpl(value: number): void { - this.camera.near = this.camDistance - value; + this.camera.near = ARBITRARY_CAM_DISTANCE - value; this.camera.updateProjectionMatrix(); } setAdditionalInfo(info: string): void { this.additionalInfo = info; } + + getCameraForPlane(_plane: Viewport) { + return this.camera; + } } export default ArbitraryView; diff --git a/frontend/javascripts/oxalis/view/context_menu.tsx b/frontend/javascripts/oxalis/view/context_menu.tsx index 88daf774cf8..eeffdde748a 100644 --- a/frontend/javascripts/oxalis/view/context_menu.tsx +++ b/frontend/javascripts/oxalis/view/context_menu.tsx @@ -25,6 +25,8 @@ import { OrthoView, AnnotationToolEnum, VolumeTools, + AltOrOptionKey, + CtrlOrCmdKey, } from "oxalis/constants"; import { V3 } from "libs/mjs"; import { @@ -473,7 +475,7 @@ function getNodeContextMenuOptions({ label: ( <> Create Edge & Merge with this Tree{" "} - {useLegacyBindings ? shortcutBuilder(["Shift", "Alt", "leftMouse"]) : null} + {useLegacyBindings ? shortcutBuilder(["Shift", AltOrOptionKey, "leftMouse"]) : null} ), }, @@ -524,7 +526,7 @@ function getNodeContextMenuOptions({ label: ( <> Delete Edge to this Node{" "} - {useLegacyBindings ? shortcutBuilder(["Shift", "Ctrl", "leftMouse"]) : null} + {useLegacyBindings ? shortcutBuilder(["Shift", CtrlOrCmdKey, "leftMouse"]) : null} ), }, @@ -904,7 +906,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] {!isAgglomerateMappingEnabled.value ? ( ) : null}{" "} - {shortcutBuilder(["SHIFT", "middleMouse"])} + {shortcutBuilder(["Shift", "middleMouse"])} ), @@ -922,7 +924,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] : "Cannot merge because the proofreading tool is not active." } > - Merge with active segment {shortcutBuilder(["SHIFT", "leftMouse"])} + Merge with active segment {shortcutBuilder(["Shift", "leftMouse"])} ), } @@ -940,7 +942,9 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] : "Cannot merge because the proofreading tool is not active." } > - Split from active segment {shortcutBuilder(["CTRL", "leftMouse"])} + + Split from active segment {shortcutBuilder([CtrlOrCmdKey, "leftMouse"])} + ), } diff --git a/frontend/javascripts/oxalis/view/input_catcher.tsx b/frontend/javascripts/oxalis/view/input_catcher.tsx index e2ac35dd579..955e414b330 100644 --- a/frontend/javascripts/oxalis/view/input_catcher.tsx +++ b/frontend/javascripts/oxalis/view/input_catcher.tsx @@ -1,7 +1,12 @@ import _ from "lodash"; import * as React from "react"; import type { Rect, Viewport } from "oxalis/constants"; -import { ArbitraryViewport } from "oxalis/constants"; +import { + AnnotationToolEnum, + ArbitraryViewport, + ArbitraryViews, + OrthoViews, +} from "oxalis/constants"; import { setInputCatcherRects } from "oxalis/model/actions/view_mode_actions"; import Scalebar from "oxalis/view/scalebar"; import ViewportStatusIndicator from "oxalis/view/viewport_status_indicator"; @@ -135,15 +140,15 @@ function InputCatcher({ const activeTool = useSelector((state: OxalisState) => state.uiInformation.activeTool); const isShiftPressed = useKeyPress("Shift"); - const isControlPressed = useKeyPress("Control"); + const isControlPressed = useKeyPress("ControlOrMeta"); const isAltPressed = useKeyPress("Alt"); - const adaptedTool = adaptActiveToolToShortcuts( - activeTool, - isShiftPressed, - isControlPressed, - isAltPressed, - ); + const adaptedTool = + viewportID === ArbitraryViews.arbitraryViewport + ? AnnotationToolEnum.SKELETON + : viewportID === OrthoViews.TDView + ? AnnotationToolEnum.MOVE + : adaptActiveToolToShortcuts(activeTool, isShiftPressed, isControlPressed, isAltPressed); return (
{ return ( {isUpdateTracingAllowed ? : null} diff --git a/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx b/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx index c42fd7ddd3a..a465864febe 100644 --- a/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx +++ b/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx @@ -579,7 +579,7 @@ class DatasetSettings extends React.PureComponent { }; const onChange = (value: boolean, event: React.MouseEvent) => { - if (!event.ctrlKey && !event.altKey && !event.shiftKey) { + if (!event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) { setSingleLayerVisibility(value); return; } diff --git a/frontend/javascripts/oxalis/view/plane_view.ts b/frontend/javascripts/oxalis/view/plane_view.ts index 44a31f2c6c7..19aad397297 100644 --- a/frontend/javascripts/oxalis/view/plane_view.ts +++ b/frontend/javascripts/oxalis/view/plane_view.ts @@ -5,7 +5,7 @@ import _ from "lodash"; import { getGroundTruthLayoutRect } from "oxalis/view/layouting/default_layout_configs"; import { getInputCatcherRect } from "oxalis/model/accessors/view_mode_accessor"; import { updateTemporarySettingAction } from "oxalis/model/actions/settings_actions"; -import type { OrthoViewMap, Vector3 } from "oxalis/constants"; +import type { OrthoViewMap, Vector3, Viewport } from "oxalis/constants"; import Constants, { OrthoViewColors, OrthoViewValues, OrthoViews } from "oxalis/constants"; import Store from "oxalis/store"; import app from "app"; @@ -257,6 +257,13 @@ class PlaneView { ), ); } + + getCameraForPlane(plane: Viewport) { + if (plane === "arbitraryViewport") { + throw new Error("Cannot access camera for arbitrary viewport."); + } + return this.getCameras()[plane]; + } } export default PlaneView; diff --git a/frontend/javascripts/oxalis/view/rendering_utils.ts b/frontend/javascripts/oxalis/view/rendering_utils.ts index afadc8a1e32..db204acabfe 100644 --- a/frontend/javascripts/oxalis/view/rendering_utils.ts +++ b/frontend/javascripts/oxalis/view/rendering_utils.ts @@ -39,7 +39,8 @@ export const clearCanvas = (renderer: THREE.WebGLRenderer) => { export function renderToTexture( plane: OrthoView | typeof ArbitraryViewport, scene?: THREE.Scene, - camera?: THREE.OrthographicCamera, // When withFarClipping is true, the user-specified clipping distance is used. + camera?: THREE.OrthographicCamera | THREE.PerspectiveCamera, + // When withFarClipping is true, the user-specified clipping distance is used. // Note that the data planes might not be included in the rendered texture, since // these are exactly offset by the clipping distance. Currently, `withFarClipping` // is only used for node picking (which does not render the data planes), which is why @@ -51,19 +52,32 @@ export function renderToTexture( const { renderer, scene: defaultScene } = SceneController; const state = Store.getState(); scene = scene || defaultScene; - camera = (camera || scene.getObjectByName(plane)) as THREE.OrthographicCamera; + camera = (camera || scene.getObjectByName(plane)) as + | THREE.OrthographicCamera + | THREE.PerspectiveCamera; // Don't respect withFarClipping for the TDViewport as we don't do any clipping for // nodes there. if (withFarClipping && plane !== OrthoViews.TDView) { - const isArbitraryMode = constants.MODES_ARBITRARY.includes( - state.temporaryConfiguration.viewMode, - ); - camera = camera.clone() as THREE.OrthographicCamera; - camera.far = isArbitraryMode - ? state.userConfiguration.clippingDistanceArbitrary - : state.userConfiguration.clippingDistance; - camera.updateProjectionMatrix(); + function adaptCameraToCurrentClippingDistance( + camera: THREE.OrthographicCamera | THREE.PerspectiveCamera, + ) { + const isArbitraryMode = constants.MODES_ARBITRARY.includes( + state.temporaryConfiguration.viewMode, + ); + camera = camera.clone(); + if (isArbitraryMode) { + camera.far = state.userConfiguration.clippingDistanceArbitrary; + } else { + // The near value is already set in the camera (done in the CameraController). + // The far value has to be set, since in normal rendering the far clipping is + // achieved by offsetting the plane instead of setting the far property. + camera.far = state.userConfiguration.clippingDistance; + } + camera.updateProjectionMatrix(); + } + + adaptCameraToCurrentClippingDistance(camera); } clearColor = clearColor != null ? clearColor : 0x000000; @@ -110,10 +124,9 @@ export async function downloadScreenshot() { for (const planeId of planeIds) { const { width, height } = getInputCatcherRect(Store.getState(), planeId); if (width === 0 || height === 0) continue; - // @ts-ignore planeId cannot be arbitraryViewport in OrthoViewColors access - const clearColor = OrthoViewValues.includes(planeId) ? OrthoViewColors[planeId] : 0xffffff; - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'null' is not assignable to param... Remove this comment to see the full error message - const buffer = renderToTexture(planeId, null, null, false, clearColor); + const clearColor = planeId !== "arbitraryViewport" ? OrthoViewColors[planeId] : 0xffffff; + + const buffer = renderToTexture(planeId, undefined, undefined, false, clearColor); const inputCatcherElement = document.querySelector(`#inputcatcher_${planeId}`); const drawImageIntoCanvasCallback = diff --git a/frontend/javascripts/oxalis/view/statusbar.tsx b/frontend/javascripts/oxalis/view/statusbar.tsx index a42a3d17271..4dbb53bebea 100644 --- a/frontend/javascripts/oxalis/view/statusbar.tsx +++ b/frontend/javascripts/oxalis/view/statusbar.tsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from "react-redux"; import React, { useCallback, useState } from "react"; import { WarningOutlined, MoreOutlined, DownloadOutlined } from "@ant-design/icons"; import type { Vector3 } from "oxalis/constants"; -import { OrthoViews } from "oxalis/constants"; +import { AltOrOptionKey, OrthoViews } from "oxalis/constants"; import { getVisibleSegmentationLayer, hasVisibleUint64Segmentation, @@ -76,7 +76,7 @@ function ZoomShortcut() { top: -2, }} > - Alt + {AltOrOptionKey} {" "} + @@ -159,21 +159,10 @@ function ShortcutsInfo() { ); const isPlaneMode = useSelector((state: OxalisState) => getIsPlaneMode(state)); const isShiftPressed = useKeyPress("Shift"); - const isControlPressed = useKeyPress("Control"); + const isControlOrMetaPressed = useKeyPress("ControlOrMeta"); const isAltPressed = useKeyPress("Alt"); - const adaptedTool = adaptActiveToolToShortcuts( - activeTool, - isShiftPressed, - isControlPressed, - isAltPressed, - ); - const actionDescriptor = getToolClassForAnnotationTool(adaptedTool).getActionDescriptors( - adaptedTool, - useLegacyBindings, - isShiftPressed, - isControlPressed, - isAltPressed, - ); + const hasSkeleton = useSelector((state: OxalisState) => state.tracing.skeleton != null); + const moreShortcutsLink = ( - - Mouse Left Drag - Move - + {actionDescriptor != null ? ( + + ) : ( + + Mouse Left Drag + Move + + )} + @@ -317,7 +336,7 @@ function ShortcutsInfo() { src="/assets/images/icon-statusbar-mouse-wheel.svg" alt="Mouse Wheel" /> - {isAltPressed || isControlPressed ? "Zoom in/out" : "Move along 3rd axis"} + {isAltPressed || isControlOrMetaPressed ? "Zoom in/out" : "Move along 3rd axis"} .ant-select-selector { - background-color: @dark-bg; + background-color: @dark-bg !important; color: @dark-fg !important; border-color: @dark-border;