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

View File

@ -25,6 +25,8 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []); const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []);
const handleHoverOut = useCallback(() => setHoverID(undefined), []); const handleHoverOut = useCallback(() => setHoverID(undefined), []);
const [isDragging, setIsDragging] = useState(false);
return ( return (
<Modal <Modal
readonly 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' className='px-2 py-1 rounded-2xl cc-blur max-w-[60ch] text-lg text-center'
style={{ backgroundColor: colors.bgBlur }} style={{ backgroundColor: colors.bgBlur }}
> >
{!hoverNode ? expression : null} {!hoverNode || isDragging ? expression : null}
{hoverNode ? ( {!isDragging && hoverNode ? (
<div> <div>
<span>{expression.slice(0, hoverNode.start)}</span> <span>{expression.slice(0, hoverNode.start)}</span>
<span className='clr-selected'>{expression.slice(hoverNode.start, hoverNode.finish)}</span> <span className='clr-selected'>{expression.slice(hoverNode.start, hoverNode.finish)}</span>
@ -47,7 +49,12 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
) : null} ) : null}
</Overlay> </Overlay>
<ReactFlowProvider> <ReactFlowProvider>
<ASTFlow data={syntaxTree} onNodeEnter={handleHoverIn} onNodeLeave={handleHoverOut} /> <ASTFlow
data={syntaxTree}
onNodeEnter={handleHoverIn}
onNodeLeave={handleHoverOut}
onChangeDragging={setIsDragging}
/>
</ReactFlowProvider> </ReactFlowProvider>
</Modal> </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 { EdgeTypes } from 'reactflow';
import ASTEdge from './ASTEdge'; import DynamicEdge from '@/components/ui/Flow/DynamicEdge';
export const ASTEdgeTypes: EdgeTypes = { 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 { Edge, Node } from 'reactflow';
import { ISyntaxTreeNode } from '@/models/rslang'; import { ISyntaxTreeNode } from '@/models/rslang';
import { PARAMETER } from '@/utils/constants';
const NODE_WIDTH = 44;
const NODE_HEIGHT = 44;
const HOR_SEPARATION = 40;
const VERT_SEPARATION = 40;
export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) { export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({ dagreGraph.setGraph({
rankdir: 'TB', rankdir: 'TB',
ranksep: VERT_SEPARATION, ranksep: 40,
nodesep: HOR_SEPARATION, nodesep: 40,
ranker: 'network-simplex', ranker: 'network-simplex',
align: undefined align: undefined
}); });
nodes.forEach(node => { 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 => { edges.forEach(edge => {
@ -29,7 +25,7 @@ export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
nodes.forEach(node => { nodes.forEach(node => {
const nodeWithPosition = dagreGraph.node(node.id); const nodeWithPosition = dagreGraph.node(node.id);
node.position.x = nodeWithPosition.x - NODE_WIDTH / 2; node.position.x = nodeWithPosition.x - PARAMETER.graphNodeRadius;
node.position.y = nodeWithPosition.y - NODE_HEIGHT / 2; node.position.y = nodeWithPosition.y - PARAMETER.graphNodeRadius;
}); });
} }

View File

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

View File

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

View File

@ -87,7 +87,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
'text-sm', 'text-sm',
'cc-scroll-y' '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} initial={false}
animate={!isFolded ? 'open' : 'closed'} animate={!isFolded ? 'open' : 'closed'}
variants={animateDropdown} 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 { EdgeTypes } from 'reactflow';
import TGEdge from './TGEdge'; import DynamicEdge from '../../../../components/ui/Flow/DynamicEdge';
export const TGEdgeTypes: EdgeTypes = { 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 }} /> <Handle type='target' position={Position.Top} style={{ opacity: 0 }} />
<div <div
className='w-full h-full cursor-default flex items-center justify-center rounded-full' className='w-full h-full cursor-default flex items-center justify-center rounded-full'
style={{ style={{ backgroundColor: !node.selected ? node.data.fill : colors.bgActiveSelection }}
backgroundColor: !node.selected ? node.data.fill : colors.bgActiveSelection,
outlineOffset: '4px',
outlineStyle: 'solid',
outlineColor: node.selected ? colors.bgActiveSelection : 'transparent'
}}
> >
<div className='absolute top-[9px] left-0 text-center w-full'>{node.data.label}</div> <div className='absolute top-[9px] left-0 text-center w-full'>{node.data.label}</div>
<div <div

View File

@ -97,10 +97,6 @@
box-shadow: 0 0 0 2px var(--cl-prim-bg-80) !important; box-shadow: 0 0 0 2px var(--cl-prim-bg-80) !important;
} }
&.selected {
border-color: var(--cd-bg-40);
}
.dark & { .dark & {
color: var(--cd-fg-100); color: var(--cd-fg-100);
border-color: var(--cd-bg-40); border-color: var(--cd-bg-40);
@ -109,10 +105,6 @@
&:hover:not(.selected) { &:hover:not(.selected) {
box-shadow: 0 0 0 3px var(--cd-prim-bg-80) !important; 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; padding: 2px;
width: 150px; width: 150px;
height: 40px; height: 40px;
&.selected {
border-color: var(--cd-bg-40);
}
.dark & {
&.selected {
border-color: var(--cl-bg-40);
}
}
} }
.react-flow__node-step, .react-flow__node-step,
@ -134,10 +136,19 @@
border-radius: 100%; border-radius: 100%;
width: 40px; width: 40px;
height: 40px; height: 40px;
}
.react-flow__node-concept { outline-offset: 4px;
outline-style: solid;
outline-color: transparent;
&.selected { &.selected {
outline-color: var(--cl-teal-bg-100);
border-color: transparent; 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 ossDistanceX: 180, // pixels - insert x-distance between node centers
ossDistanceY: 100, // pixels - insert y-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 graphNodeRadius: 20, // pixels - radius of graph node
graphNodePadding: 5, // pixels - padding 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 graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be