mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 04:40: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> – перемещение экрана
|
<kbd>Space</kbd> – перемещение экрана
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<kbd>Shift</kbd> – перемещение в границах блока
|
<kbd>Shift</kbd> – расширять границы
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>Стрелки</kbd> – выделение
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
type IOssLayout
|
type IOssLayout
|
||||||
} from '../backend/types';
|
} from '../backend/types';
|
||||||
|
|
||||||
import { type IOperationSchema } from './oss';
|
import { type IOperationSchema, NodeType } from './oss';
|
||||||
import { type Position2D, type Rectangle2D } from './oss-layout';
|
import { type Position2D, type Rectangle2D } from './oss-layout';
|
||||||
|
|
||||||
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
||||||
|
@ -132,6 +132,100 @@ export class LayoutManager {
|
||||||
this.extendParentBounds(parentNode, targetNode);
|
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) {
|
private extendParentBounds(parent: INodePosition | null, child: Rectangle2D) {
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
return;
|
return;
|
||||||
|
@ -264,3 +358,16 @@ function calculatePositionFromChildren(
|
||||||
height: bottom - top
|
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';
|
'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 { type Edge, type Node, useEdgesState, useNodesState, useOnSelectionChange, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
@ -18,8 +18,8 @@ const Z_BLOCK = 1;
|
||||||
const Z_SCHEMA = 10;
|
const Z_SCHEMA = 10;
|
||||||
|
|
||||||
export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
const { schema, setSelected } = useOssEdit();
|
const { schema, selected, setSelected } = useOssEdit();
|
||||||
const { fitView } = useReactFlow();
|
const { fitView, viewportInitialized } = useReactFlow();
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||||
|
|
||||||
|
@ -95,6 +95,20 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
|
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 (
|
return (
|
||||||
<OssFlowContext
|
<OssFlowContext
|
||||||
value={{
|
value={{
|
||||||
|
|
|
@ -45,7 +45,8 @@ export const flowOptions = {
|
||||||
|
|
||||||
export function OssFlow() {
|
export function OssFlow() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
const { navigateOperationSchema, schema, selected, selectedItems, isMutable, canDeleteOperation } = useOssEdit();
|
const { navigateOperationSchema, schema, selected, setSelected, selectedItems, isMutable, canDeleteOperation } =
|
||||||
|
useOssEdit();
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
|
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
|
||||||
const store = useStoreApi();
|
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>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
return;
|
return;
|
||||||
|
@ -202,10 +250,27 @@ export function OssFlow() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.key === 'Delete') {
|
if (event.code === 'Delete') {
|
||||||
withPreventDefault(handleDeleteSelected)(event);
|
withPreventDefault(handleDeleteSelected)(event);
|
||||||
return;
|
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>) {
|
function handleMouseMove(event: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user