M: Update GraphUI
This commit is contained in:
parent
82731be327
commit
a63af2b5cf
36
rsconcept/frontend/src/components/ui/Flow/DynamicEdge.tsx
Normal file
36
rsconcept/frontend/src/components/ui/Flow/DynamicEdge.tsx
Normal 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;
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ interface ASTNodeInternal {
|
|||
id: string;
|
||||
data: ISyntaxTreeNode;
|
||||
dragging: boolean;
|
||||
selected: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}
|
||||
|
@ -32,10 +33,18 @@ 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 }}
|
||||
>
|
||||
{label}
|
||||
<div className='absolute top-0 left-0 text-center w-full'>{label}</div>
|
||||
<div
|
||||
style={{
|
||||
WebkitTextStrokeWidth: 2,
|
||||
WebkitTextStrokeColor: colors.bgDefault
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user