mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-13 20:30:36 +03:00
F: Implement hotkey navigation
This commit is contained in:
parent
de108074b1
commit
05275f87af
|
@ -111,7 +111,10 @@ export function HelpOssGraph() {
|
|||
<kbd>Space</kbd> – перемещение экрана
|
||||
</li>
|
||||
<li>
|
||||
<kbd>Shift</kbd> – перемещение в границах блока
|
||||
<kbd>Shift</kbd> – расширять границы
|
||||
</li>
|
||||
<li>
|
||||
<kbd>Стрелки</kbd> – выделение
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
type IOssLayout
|
||||
} from '../backend/types';
|
||||
|
||||
import { type IOperationSchema } from './oss';
|
||||
import { type IOperationSchema, NodeType } from './oss';
|
||||
import { type Position2D, type Rectangle2D } from './oss-layout';
|
||||
|
||||
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
||||
|
@ -132,6 +132,100 @@ export class LayoutManager {
|
|||
this.extendParentBounds(parentNode, targetNode);
|
||||
}
|
||||
|
||||
/** Calculate closest node to the left */
|
||||
selectLeft(targetID: string): string | null {
|
||||
const targetNode = this.layout.find(pos => pos.nodeID === targetID);
|
||||
if (!targetNode) {
|
||||
return null;
|
||||
}
|
||||
const operationNodes = this.layout.filter(pos => pos.nodeID !== targetID && pos.nodeID.startsWith('o'));
|
||||
const leftNodes = operationNodes.filter(pos => pos.x <= targetNode.x);
|
||||
if (leftNodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const similarYNodes = leftNodes.filter(pos => Math.abs(pos.y - targetNode.y) <= MIN_DISTANCE);
|
||||
let closestNode: typeof targetNode | null = null;
|
||||
if (similarYNodes.length > 0) {
|
||||
closestNode = similarYNodes.reduce((prev, curr) => (curr.x > prev.x ? curr : prev));
|
||||
} else {
|
||||
closestNode = findClosestNodeByDistance(leftNodes, targetNode);
|
||||
}
|
||||
return closestNode?.nodeID ?? null;
|
||||
}
|
||||
|
||||
/** Calculate closest node to the right */
|
||||
selectRight(targetID: string): string | null {
|
||||
const targetNode = this.layout.find(pos => pos.nodeID === targetID);
|
||||
if (!targetNode) {
|
||||
return null;
|
||||
}
|
||||
const operationNodes = this.layout.filter(pos => pos.nodeID !== targetID && pos.nodeID.startsWith('o'));
|
||||
const rightNodes = operationNodes.filter(pos => pos.x >= targetNode.x);
|
||||
if (rightNodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const similarYNodes = rightNodes.filter(pos => Math.abs(pos.y - targetNode.y) <= MIN_DISTANCE);
|
||||
let closestNode: typeof targetNode | null = null;
|
||||
if (similarYNodes.length > 0) {
|
||||
closestNode = similarYNodes.reduce((prev, curr) => (curr.x < prev.x ? curr : prev));
|
||||
} else {
|
||||
closestNode = findClosestNodeByDistance(rightNodes, targetNode);
|
||||
}
|
||||
return closestNode?.nodeID ?? null;
|
||||
}
|
||||
|
||||
/** Calculate closest node upwards */
|
||||
selectUp(targetID: string): string | null {
|
||||
const targetNode = this.layout.find(pos => pos.nodeID === targetID);
|
||||
if (!targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const operationNodes = this.layout.filter(pos => pos.nodeID !== targetID && pos.nodeID.startsWith('o'));
|
||||
const upperNodes = operationNodes.filter(pos => pos.y <= targetNode.y - MIN_DISTANCE);
|
||||
const targetOperation = this.oss.itemByNodeID.get(targetID);
|
||||
if (upperNodes.length === 0 || !targetOperation || targetOperation.nodeType === NodeType.BLOCK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const predecessors = this.oss.graph.expandAllInputs([targetOperation.id]);
|
||||
const predecessorNodes = upperNodes.filter(pos => predecessors.includes(Number(pos.nodeID.slice(1))));
|
||||
|
||||
let closestNode: typeof targetNode | null = null;
|
||||
if (predecessorNodes.length > 0) {
|
||||
closestNode = findClosestNodeByDistance(predecessorNodes, targetNode);
|
||||
} else {
|
||||
closestNode = findClosestNodeByDistance(upperNodes, targetNode);
|
||||
}
|
||||
return closestNode?.nodeID ?? null;
|
||||
}
|
||||
|
||||
/** Calculate closest node downwards */
|
||||
selectDown(targetID: string): string | null {
|
||||
const targetNode = this.layout.find(pos => pos.nodeID === targetID);
|
||||
if (!targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const operationNodes = this.layout.filter(pos => pos.nodeID !== targetID && pos.nodeID.startsWith('o'));
|
||||
const lowerNodes = operationNodes.filter(pos => pos.y >= targetNode.y - MIN_DISTANCE);
|
||||
const targetOperation = this.oss.itemByNodeID.get(targetID);
|
||||
if (lowerNodes.length === 0 || !targetOperation || targetOperation.nodeType === NodeType.BLOCK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const descendants = this.oss.graph.expandAllOutputs([targetOperation.id]);
|
||||
const descendantsNodes = lowerNodes.filter(pos => descendants.includes(Number(pos.nodeID.slice(1))));
|
||||
|
||||
let closestNode: typeof targetNode | null = null;
|
||||
if (descendantsNodes.length > 0) {
|
||||
closestNode = findClosestNodeByDistance(descendantsNodes, targetNode);
|
||||
} else {
|
||||
closestNode = findClosestNodeByDistance(lowerNodes, targetNode);
|
||||
}
|
||||
return closestNode?.nodeID ?? null;
|
||||
}
|
||||
|
||||
private extendParentBounds(parent: INodePosition | null, child: Rectangle2D) {
|
||||
if (!parent) {
|
||||
return;
|
||||
|
@ -264,3 +358,16 @@ function calculatePositionFromChildren(
|
|||
height: bottom - top
|
||||
};
|
||||
}
|
||||
|
||||
function findClosestNodeByDistance(nodes: INodePosition[], target: INodePosition): INodePosition | null {
|
||||
let minDist = Infinity;
|
||||
let minNode = null;
|
||||
for (const curr of nodes) {
|
||||
const currDist = Math.hypot(curr.x - target.x, curr.y - target.y);
|
||||
if (currDist < minDist) {
|
||||
minDist = currDist;
|
||||
minNode = curr;
|
||||
}
|
||||
}
|
||||
return minNode;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type Edge, type Node, useEdgesState, useNodesState, useOnSelectionChange, useReactFlow } from 'reactflow';
|
||||
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
@ -18,8 +18,8 @@ const Z_BLOCK = 1;
|
|||
const Z_SCHEMA = 10;
|
||||
|
||||
export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||
const { schema, setSelected } = useOssEdit();
|
||||
const { fitView } = useReactFlow();
|
||||
const { schema, selected, setSelected } = useOssEdit();
|
||||
const { fitView, viewportInitialized } = useReactFlow();
|
||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||
|
||||
|
@ -95,6 +95,20 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
|||
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
|
||||
}
|
||||
|
||||
const prevSelected = useRef<string[]>([]);
|
||||
if (
|
||||
viewportInitialized &&
|
||||
(prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]))
|
||||
) {
|
||||
prevSelected.current = selected;
|
||||
setNodes(prev =>
|
||||
prev.map(node => ({
|
||||
...node,
|
||||
selected: selected.includes(node.id)
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OssFlowContext
|
||||
value={{
|
||||
|
|
|
@ -45,7 +45,8 @@ export const flowOptions = {
|
|||
|
||||
export function OssFlow() {
|
||||
const mainHeight = useMainHeight();
|
||||
const { navigateOperationSchema, schema, selected, selectedItems, isMutable, canDeleteOperation } = useOssEdit();
|
||||
const { navigateOperationSchema, schema, selected, setSelected, selectedItems, isMutable, canDeleteOperation } =
|
||||
useOssEdit();
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
|
||||
const store = useStoreApi();
|
||||
|
@ -169,6 +170,53 @@ export function OssFlow() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleSelectLeft() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectLeft(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectRight() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectRight(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
function handleSelectUp() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectUp(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectDown() {
|
||||
const selectedOperation = selectedItems.find(item => item.nodeType === NodeType.OPERATION);
|
||||
if (!selectedOperation) {
|
||||
return;
|
||||
}
|
||||
const manager = new LayoutManager(schema, getLayout());
|
||||
const newNodeID = manager.selectDown(selectedOperation.nodeID);
|
||||
if (newNodeID) {
|
||||
setSelected([newNodeID]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
|
@ -202,10 +250,27 @@ export function OssFlow() {
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (event.key === 'Delete') {
|
||||
if (event.code === 'Delete') {
|
||||
withPreventDefault(handleDeleteSelected)(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 'ArrowLeft') {
|
||||
withPreventDefault(handleSelectLeft)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowRight') {
|
||||
withPreventDefault(handleSelectRight)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowUp') {
|
||||
withPreventDefault(handleSelectUp)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'ArrowDown') {
|
||||
withPreventDefault(handleSelectDown)(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(event: React.MouseEvent<HTMLDivElement>) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user