M: Update GraphUI

This commit is contained in:
Ivan 2024-12-02 20:46:54 +03:00
parent 82731be327
commit a63af2b5cf
14 changed files with 106 additions and 104 deletions

View File

@ -0,0 +1,36 @@
import { EdgeProps, getStraightPath } from 'reactflow';
import { PARAMETER } from '@/utils/constants';
const RADIUS = PARAMETER.graphNodeRadius + PARAMETER.graphNodePadding;
function DynamicEdge({ id, markerEnd, style, ...props }: EdgeProps) {
const sourceY = props.sourceY - PARAMETER.graphNodeRadius - PARAMETER.graphHandleSize;
const targetY = props.targetY + PARAMETER.graphNodeRadius + PARAMETER.graphHandleSize;
const dx = props.targetX - props.sourceX;
const dy = targetY - sourceY;
const distance = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
if (distance <= 2 * RADIUS) {
return null;
}
const ux = dx / distance;
const uy = dy / distance;
const [path] = getStraightPath({
sourceX: props.sourceX + ux * RADIUS,
sourceY: sourceY + uy * RADIUS,
targetX: props.targetX - ux * RADIUS,
targetY: targetY - uy * RADIUS
});
return (
<>
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
</>
);
}
export default DynamicEdge;

View File

@ -13,9 +13,10 @@ interface ASTFlowProps {
data: SyntaxTree;
onNodeEnter: (node: Node) => void;
onNodeLeave: (node: Node) => void;
onChangeDragging: (value: boolean) => void;
}
function ASTFlow({ data, onNodeEnter, onNodeLeave }: ASTFlowProps) {
function ASTFlow({ data, onNodeEnter, onNodeLeave, onChangeDragging }: ASTFlowProps) {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges] = useEdgesState([]);
@ -59,6 +60,8 @@ function ASTFlow({ data, onNodeEnter, onNodeLeave }: ASTFlowProps) {
nodesFocusable={false}
onNodeMouseEnter={(_, node) => onNodeEnter(node)}
onNodeMouseLeave={(_, node) => onNodeLeave(node)}
onNodeDragStart={() => onChangeDragging(true)}
onNodeDragStop={() => onChangeDragging(false)}
onNodesChange={onNodesChange}
nodeTypes={ASTNodeTypes}
edgeTypes={ASTEdgeTypes}

View File

@ -25,6 +25,8 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []);
const handleHoverOut = useCallback(() => setHoverID(undefined), []);
const [isDragging, setIsDragging] = useState(false);
return (
<Modal
readonly
@ -37,8 +39,8 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
className='px-2 py-1 rounded-2xl cc-blur max-w-[60ch] text-lg text-center'
style={{ backgroundColor: colors.bgBlur }}
>
{!hoverNode ? expression : null}
{hoverNode ? (
{!hoverNode || isDragging ? expression : null}
{!isDragging && hoverNode ? (
<div>
<span>{expression.slice(0, hoverNode.start)}</span>
<span className='clr-selected'>{expression.slice(hoverNode.start, hoverNode.finish)}</span>
@ -47,7 +49,12 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
) : null}
</Overlay>
<ReactFlowProvider>
<ASTFlow data={syntaxTree} onNodeEnter={handleHoverIn} onNodeLeave={handleHoverOut} />
<ASTFlow
data={syntaxTree}
onNodeEnter={handleHoverIn}
onNodeLeave={handleHoverOut}
onChangeDragging={setIsDragging}
/>
</ReactFlowProvider>
</Modal>
);

View File

@ -1,28 +0,0 @@
import { EdgeProps, getStraightPath } from 'reactflow';
const NODE_RADIUS = 20;
const EDGE_RADIUS = 25;
function ASTEdge({ id, markerEnd, style, ...props }: EdgeProps) {
const scale =
EDGE_RADIUS /
Math.sqrt(
Math.pow(props.sourceX - props.targetX, 2) +
Math.pow(Math.abs(props.sourceY - props.targetY) + 2 * NODE_RADIUS, 2)
);
const [path] = getStraightPath({
sourceX: props.sourceX - (props.sourceX - props.targetX) * scale,
sourceY: props.sourceY - (props.sourceY - props.targetY - 2 * NODE_RADIUS) * scale - NODE_RADIUS,
targetX: props.targetX + (props.sourceX - props.targetX) * scale,
targetY: props.targetY + (props.sourceY - props.targetY - 2 * NODE_RADIUS) * scale + NODE_RADIUS
});
return (
<>
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
</>
);
}
export default ASTEdge;

View File

@ -1,7 +1,7 @@
import { EdgeTypes } from 'reactflow';
import ASTEdge from './ASTEdge';
import DynamicEdge from '@/components/ui/Flow/DynamicEdge';
export const ASTEdgeTypes: EdgeTypes = {
dynamic: ASTEdge
dynamic: DynamicEdge
};

View File

@ -2,23 +2,19 @@ import dagre from '@dagrejs/dagre';
import { Edge, Node } from 'reactflow';
import { ISyntaxTreeNode } from '@/models/rslang';
const NODE_WIDTH = 44;
const NODE_HEIGHT = 44;
const HOR_SEPARATION = 40;
const VERT_SEPARATION = 40;
import { PARAMETER } from '@/utils/constants';
export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({
rankdir: 'TB',
ranksep: VERT_SEPARATION,
nodesep: HOR_SEPARATION,
ranksep: 40,
nodesep: 40,
ranker: 'network-simplex',
align: undefined
});
nodes.forEach(node => {
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
dagreGraph.setNode(node.id, { width: 2 * PARAMETER.graphNodeRadius, height: 2 * PARAMETER.graphNodeRadius });
});
edges.forEach(edge => {
@ -29,7 +25,7 @@ export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
nodes.forEach(node => {
const nodeWithPosition = dagreGraph.node(node.id);
node.position.x = nodeWithPosition.x - NODE_WIDTH / 2;
node.position.y = nodeWithPosition.y - NODE_HEIGHT / 2;
node.position.x = nodeWithPosition.x - PARAMETER.graphNodeRadius;
node.position.y = nodeWithPosition.y - PARAMETER.graphNodeRadius;
});
}

View File

@ -15,6 +15,7 @@ interface ASTNodeInternal {
id: string;
data: ISyntaxTreeNode;
dragging: boolean;
selected: boolean;
xPos: number;
yPos: number;
}
@ -32,11 +33,19 @@ function ASTNode(node: ASTNodeInternal) {
/>
<Handle type='source' position={Position.Bottom} style={{ opacity: 0 }} />
<div
className='font-math mt-1 w-fit px-1 text-center translate-x-[calc(-50%+20px)]'
style={{ backgroundColor: colors.bgDefault, fontSize: label.length > 3 ? 12 : 14 }}
className='font-math mt-1 w-fit text-center translate-x-[calc(-50%+20px)]'
style={{ fontSize: label.length > 3 ? 12 : 14 }}
>
<div className='absolute top-0 left-0 text-center w-full'>{label}</div>
<div
style={{
WebkitTextStrokeWidth: 2,
WebkitTextStrokeColor: colors.bgDefault
}}
>
{label}
</div>
</div>
</>
);
}

View File

@ -61,7 +61,9 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
const [edges, setEdges] = useEdgesState([]);
const flow = useReactFlow();
const store = useStoreApi();
const { addSelectedNodes } = store.getState();
const [showParamsDialog, setShowParamsDialog] = useState(false);
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>(storage.rsgraphFilter, {
noHermits: true,
noTemplates: false,
@ -81,16 +83,13 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
allowConstant: true,
allowTheorem: true
});
const [showParamsDialog, setShowParamsDialog] = useState(false);
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
const filteredGraph = useGraphFilter(controller.schema, filterParams, focusCst);
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
const [coloring, setColoring] = useLocalStorage<GraphColoring>(storage.rsgraphColoring, 'type');
const [isDragging, setIsDragging] = useState(false);
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
const filteredGraph = useGraphFilter(controller.schema, filterParams, focusCst);
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [hoverID, setHoverID] = useState<ConstituentaID | undefined>(undefined);
const hoverCst = useMemo(() => {
return hoverID && controller.schema?.cstByID.get(hoverID);
@ -100,8 +99,6 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
const [toggleResetView, setToggleResetView] = useState(false);
const { addSelectedNodes } = store.getState();
const onSelectionChange = useCallback(
({ nodes }: { nodes: Node[] }) => {
const ids = nodes.map(node => Number(node.id));
@ -299,6 +296,8 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
const handleNodeClick = useCallback(
(event: CProps.EventMouse, cstID: ConstituentaID) => {
if (event.altKey) {
event.preventDefault();
event.stopPropagation();
handleSetFocus(cstID);
}
},
@ -445,7 +444,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
hideZero
totalCount={controller.schema?.stats?.count_all ?? 0}
selectedCount={controller.selected.length}
position='top-[4.3rem] sm:top-[2rem] left-0'
position='top-[4.3rem] left-0'
/>
{!isDragging && hoverCst && hoverCstDebounced && hoverCst === hoverCstDebounced ? (
@ -465,7 +464,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
</Overlay>
) : null}
<Overlay position='top-[8.15rem] sm:top-[5.9rem] left-0' className='flex gap-1'>
<Overlay position='top-[6.15rem] sm:top-[5.9rem] left-0' className='flex gap-1'>
<div className='flex flex-col ml-2 w-[13.5rem]'>
{selectors}
{viewHidden}

View File

@ -87,7 +87,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
'text-sm',
'cc-scroll-y'
)}
style={{ maxHeight: calculateHeight(windowSize.isSmall ? '12.rem + 2px' : '16.4rem + 2px') }}
style={{ maxHeight: calculateHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px') }}
initial={false}
animate={!isFolded ? 'open' : 'closed'}
variants={animateDropdown}

View File

@ -1,27 +0,0 @@
import { EdgeProps, getStraightPath } from 'reactflow';
import { PARAMETER } from '@/utils/constants';
function TGEdge({ id, markerEnd, style, ...props }: EdgeProps) {
const sourceY = props.sourceY - PARAMETER.graphNodeRadius;
const targetY = props.targetY + PARAMETER.graphNodeRadius;
const scale =
(PARAMETER.graphNodePadding + PARAMETER.graphNodeRadius) /
Math.sqrt(Math.pow(props.sourceX - props.targetX, 2) + Math.pow(Math.abs(sourceY - targetY), 2));
const [path] = getStraightPath({
sourceX: props.sourceX - (props.sourceX - props.targetX) * scale,
sourceY: sourceY - (sourceY - targetY) * scale,
targetX: props.targetX + (props.sourceX - props.targetX) * scale,
targetY: targetY + (sourceY - targetY) * scale
});
return (
<>
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
</>
);
}
export default TGEdge;

View File

@ -1,7 +1,7 @@
import { EdgeTypes } from 'reactflow';
import TGEdge from './TGEdge';
import DynamicEdge from '../../../../components/ui/Flow/DynamicEdge';
export const TGEdgeTypes: EdgeTypes = {
termEdge: TGEdge
termEdge: DynamicEdge
};

View File

@ -35,12 +35,7 @@ function TGNode(node: TGNodeInternal) {
<Handle type='target' position={Position.Top} style={{ opacity: 0 }} />
<div
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
style={{
backgroundColor: !node.selected ? node.data.fill : colors.bgActiveSelection,
outlineOffset: '4px',
outlineStyle: 'solid',
outlineColor: node.selected ? colors.bgActiveSelection : 'transparent'
}}
style={{ backgroundColor: !node.selected ? node.data.fill : colors.bgActiveSelection }}
>
<div className='absolute top-[9px] left-0 text-center w-full'>{node.data.label}</div>
<div

View File

@ -97,10 +97,6 @@
box-shadow: 0 0 0 2px var(--cl-prim-bg-80) !important;
}
&.selected {
border-color: var(--cd-bg-40);
}
.dark & {
color: var(--cd-fg-100);
border-color: var(--cd-bg-40);
@ -109,10 +105,6 @@
&:hover:not(.selected) {
box-shadow: 0 0 0 3px var(--cd-prim-bg-80) !important;
}
&.selected {
border-color: var(--cl-bg-40);
}
}
}
@ -124,6 +116,16 @@
padding: 2px;
width: 150px;
height: 40px;
&.selected {
border-color: var(--cd-bg-40);
}
.dark & {
&.selected {
border-color: var(--cl-bg-40);
}
}
}
.react-flow__node-step,
@ -134,10 +136,19 @@
border-radius: 100%;
width: 40px;
height: 40px;
}
.react-flow__node-concept {
outline-offset: 4px;
outline-style: solid;
outline-color: transparent;
&.selected {
outline-color: var(--cl-teal-bg-100);
border-color: transparent;
}
.dark & {
&.selected {
border-color: var(--cd-teal-bg-100);
}
}
}

View File

@ -21,6 +21,7 @@ export const PARAMETER = {
ossDistanceX: 180, // pixels - insert x-distance between node centers
ossDistanceY: 100, // pixels - insert y-distance between node centers
graphHandleSize: 3, // pixels - size of graph connection handle
graphNodeRadius: 20, // pixels - radius of graph node
graphNodePadding: 5, // pixels - padding of graph node
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be