This commit is contained in:
ditiqi 2025-03-02 17:53:04 +08:00
parent eba90139e4
commit 37eff0e67c
207 changed files with 3314 additions and 4668 deletions

View File

@ -1,105 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { addEdge, ReactFlow, Background, Controls, Edge, Node, ReactFlowProvider, useEdgesState, useNodesState, MiniMap, Panel, BackgroundVariant, ControlButton, applyNodeChanges, applyEdgeChanges, SelectionMode, OnNodesChange, OnEdgesChange, useReactFlow, useOnSelectionChange, useNodesInitialized } from '@xyflow/react';
import { Button } from '../../element/Button';
import '@xyflow/react/dist/style.css';
import { edgeTypes, GraphState, nodeTypes } from './types';
import useGraphStore from './store';
import { shallow } from 'zustand/shallow';
import { useKeyboardCtrl } from './useKeyboardCtrl';
import { getMindMapLayout } from './layout';
const selector = (store: GraphState) => ({
nodes: store.present.nodes,
edges: store.present.edges,
setNodes: store.setNodes,
setEdges: store.setEdges,
record: store.record,
onNodesChange: store.onNodesChange,
onEdgesChange: store.onEdgesChange,
});
const panOnDrag = [1, 2];
const Flow: React.FC = () => {
const store = useGraphStore(selector, shallow);
useKeyboardCtrl()
const nodesInitialized = useNodesInitialized();
const onLayout = useCallback(async () => {
const layouted = getMindMapLayout({ nodes: store.nodes, edges: store.edges })
store.setNodes(layouted.nodes)
store.setEdges(layouted.edges)
}, [store.nodes, store.edges]);
useEffect(() => {
if (nodesInitialized && store.nodes.length) {
console.log('layout')
onLayout()
}
}, [nodesInitialized, store.nodes.length]);
return (
<ReactFlow
nodesDraggable={true}
nodes={store.nodes}
edges={store.edges}
onNodesChange={(changes) => {
const recordTypes = new Set(['remove', 'select']);
const undoChanges = changes.filter(change => recordTypes.has(change.type))
const otherChanges = changes.filter(change => !recordTypes.has(change.type))
if (undoChanges.length)
store.record(() => {
store.onNodesChange(undoChanges);
});
store.onNodesChange(otherChanges);
}}
onEdgesChange={(changes) => {
const recordTypes = new Set(['remove', 'select']);
changes.forEach((change) => {
if (recordTypes.has(change.type)) {
store.record(() => {
store.onEdgesChange([change]);
});
} else {
store.onEdgesChange([change]);
}
});
}}
selectionOnDrag
panOnDrag={panOnDrag}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
selectionMode={SelectionMode.Partial}
fitView
minZoom={0.001}
maxZoom={1000}
>
<Panel position="top-right">
<div className='flex items-center gap-4'>
<Button onClick={onLayout}></Button>
<span>{store.nodes.length}</span>
<span>{store.edges.length}</span>
</div>
</Panel>
<Background variant={BackgroundVariant.Dots} />
<Controls >
<ControlButton></ControlButton>
</Controls>
<MiniMap pannable zoomable nodeStrokeWidth={3} position='bottom-right'></MiniMap>
</ReactFlow>
);
};
const GraphEditor: React.FC = () => {
return (
<ReactFlowProvider>
<Flow></Flow>
</ReactFlowProvider>
);
};
export default GraphEditor;

View File

@ -1,57 +0,0 @@
import { MarkerType } from "@xyflow/react";
// 生成思维导图数据的函数
function generateMindMapData(levels: number, nodesPerLevel: number) {
const nodes = [];
const edges = [];
// 添加根节点
nodes.push({
id: 'root',
data: { label: '核心主题', level: 0 },
type: 'graph-node',
position: { x: 0, y: 0 }
});
// 为每一层生成节点
for (let level = 1; level <= levels; level++) {
const angleStep = (2 * Math.PI) / nodesPerLevel;
const radius = level * 200; // 每层的半径
for (let i = 0; i < nodesPerLevel; i++) {
const angle = i * angleStep;
const nodeId = `node-${level}-${i}`;
// 计算节点位置
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
// 添加节点
nodes.push({
id: nodeId,
data: { label: `主题${level}-${i}`, level },
type: 'graph-node',
position: { x, y }
});
// 添加边
// 第一层连接到根节点,其他层连接到上一层的节点
const sourceId = level === 1 ? 'root' : `node-${level - 1}-${Math.floor(i / 2)}`;
edges.push({
id: `edge-${level}-${i}`,
source: sourceId,
target: nodeId,
type: 'graph-edge',
});
}
}
return { nodes, edges };
}
// 生成测试数据 - 可以调整参数来控制规模
// 参数1: 层级数量
// 参数2: 每层节点数量
const { nodes: initialNodes, edges: initialEdges } = generateMindMapData(2, 3);
export { initialNodes, initialEdges };

View File

@ -1,83 +0,0 @@
import { BaseEdge, Node, Edge, EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, getStraightPath, Position, useReactFlow, useInternalNode, InternalNode } from '@xyflow/react';
export type GraphEdge = Edge<{ text: string }, 'graph-edge'>;
function getEdgeParams(sourceNode: InternalNode, targetNode: InternalNode) {
console.log(sourceNode)
const sourceCenter = {
x: sourceNode.position.x + sourceNode.width / 2,
y: sourceNode.position.y + sourceNode.height / 2,
};
const targetCenter = {
x: targetNode.position.x + targetNode.width / 2,
y: targetNode.position.y + targetNode.height / 2,
};
const dx = targetCenter.x - sourceCenter.x;
// 简化连接逻辑只基于x轴方向判断
let sourcePos: Position;
let targetPos: Position;
// 如果目标在源节点右边,源节点用右侧连接点,目标节点用左侧连接点
if (dx > 0) {
sourcePos = Position.Right;
targetPos = Position.Left;
} else {
// 如果目标在源节点左边,源节点用左侧连接点,目标节点用右侧连接点
sourcePos = Position.Left;
targetPos = Position.Right;
}
// 使用节点中心的y坐标
return {
sourcePos,
targetPos,
sx: sourceCenter.x + sourceNode.measured.width / 2,
sy: sourceCenter.y,
tx: targetCenter.x - targetNode.measured.width / 2,
ty: targetCenter.y,
};
}
export const GraphEdge = ({ id, source, target, sourceX, sourceY, targetX, targetY, data, ...props }: EdgeProps<GraphEdge>) => {
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
const { sx, sy, tx, ty, targetPos, sourcePos } = getEdgeParams(sourceNode, targetNode)
const [edgePath, labelX, labelY] = getBezierPath({
sourceX: sx,
sourceY: sy,
targetX: tx,
targetY: ty,
sourcePosition: sourcePos,
targetPosition: targetPos,
curvature: 0.3,
});
return (
<>
<BaseEdge
path={edgePath}
style={{
strokeWidth: 2,
stroke: '#b1b1b7',
transition: 'stroke 0.3s, stroke-width 0.3s',
}}
/>
<EdgeLabelRenderer>
{data?.text && (
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
fontSize: 12,
pointerEvents: 'all',
}}
className="nodrag nopan px-2 py-1 rounded bg-white/80 shadow-sm"
>
{data.text}
</div>
)}
</EdgeLabelRenderer>
</>
);
};

View File

@ -1,219 +0,0 @@
import { areLinesReverseDirection, areLinesSameDirection } from "../edge";
import {
ControlPoint,
NodeRect,
isEqualPoint,
isSegmentCrossingRect,
} from "../point";
interface GetAStarPathParams {
/**
* Collection of potential control points between `sourceOffset` and `targetOffset`, excluding the `source` and `target` points.
*/
points: ControlPoint[];
source: ControlPoint;
target: ControlPoint;
/**
* Node size information for the `source` and `target`, used to optimize edge routing without intersecting nodes.
*/
sourceRect: NodeRect;
targetRect: NodeRect;
}
/**
* Utilizes the [A\* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm) combined with
* [Manhattan Distance](https://simple.wikipedia.org/wiki/Manhattan_distance) to find the optimal path for edges.
*
* @returns Control points including sourceOffset and targetOffset (not including source and target points).
*/
export const getAStarPath = ({
points,
source,
target,
sourceRect,
targetRect,
}: GetAStarPathParams): ControlPoint[] => {
if (points.length < 3) {
return points;
}
const start = points[0];
const end = points[points.length - 1];
const openSet: ControlPoint[] = [start];
const closedSet: Set<ControlPoint> = new Set();
const cameFrom: Map<ControlPoint, ControlPoint> = new Map();
const gScore: Map<ControlPoint, number> = new Map().set(start, 0);
const fScore: Map<ControlPoint, number> = new Map().set(
start,
heuristicCostEstimate({
from: start,
to: start,
start,
end,
source,
target,
})
);
while (openSet.length) {
let current;
let currentIdx;
let lowestFScore = Infinity;
openSet.forEach((p, idx) => {
const score = fScore.get(p) ?? 0;
if (score < lowestFScore) {
lowestFScore = score;
current = p;
currentIdx = idx;
}
});
if (!current) {
break;
}
if (current === end) {
return buildPath(cameFrom, current);
}
openSet.splice(currentIdx!, 1);
closedSet.add(current);
const curFScore = fScore.get(current) ?? 0;
const previous = cameFrom.get(current);
const neighbors = getNextNeighborPoints({
points,
previous,
current,
sourceRect,
targetRect,
});
for (const neighbor of neighbors) {
if (closedSet.has(neighbor)) {
continue;
}
const neighborGScore = gScore.get(neighbor) ?? 0;
const tentativeGScore = curFScore + estimateDistance(current, neighbor);
if (openSet.includes(neighbor) && tentativeGScore >= neighborGScore) {
continue;
}
openSet.push(neighbor);
cameFrom.set(neighbor, current);
gScore.set(neighbor, tentativeGScore);
fScore.set(
neighbor,
neighborGScore +
heuristicCostEstimate({
from: current,
to: neighbor,
start,
end,
source,
target,
})
);
}
}
return [start, end];
};
const buildPath = (
cameFrom: Map<ControlPoint, ControlPoint>,
current: ControlPoint
): ControlPoint[] => {
const path = [current];
let previous = cameFrom.get(current);
while (previous) {
path.push(previous);
previous = cameFrom.get(previous);
}
return path.reverse();
};
interface GetNextNeighborPointsParams {
points: ControlPoint[];
previous?: ControlPoint;
current: ControlPoint;
sourceRect: NodeRect;
targetRect: NodeRect;
}
/**
* Get the set of possible neighboring points for the current control point
*
* - The line is in a horizontal or vertical direction
* - The line does not intersect with the two end nodes
* - The line does not overlap with the previous line segment in reverse direction
*/
export const getNextNeighborPoints = ({
points,
previous,
current,
sourceRect,
targetRect,
}: GetNextNeighborPointsParams): ControlPoint[] => {
return points.filter((p) => {
if (p === current) {
return false;
}
// The connection is in the horizontal or vertical direction
const rightDirection = p.x === current.x || p.y === current.y;
// Reverse direction with the previous line segment (overlap)
const reverseDirection = previous
? areLinesReverseDirection(previous, current, current, p)
: false;
return (
rightDirection && // The line is in a horizontal or vertical direction
!reverseDirection && // The line does not overlap with the previous line segment in reverse direction
!isSegmentCrossingRect(p, current, sourceRect) && // Does not intersect with sourceNode
!isSegmentCrossingRect(p, current, targetRect) // Does not intersect with targetNode
);
});
};
interface HeuristicCostParams {
from: ControlPoint;
to: ControlPoint;
start: ControlPoint;
end: ControlPoint;
source: ControlPoint;
target: ControlPoint;
}
/**
* Connection point distance loss function
*
* - The smaller the sum of distances, the better
* - The closer the start and end line segments are in direction, the better
* - The better the inflection point is symmetric or centered in the line segment
*/
const heuristicCostEstimate = ({
from,
to,
start,
end,
source,
target,
}: HeuristicCostParams): number => {
const base = estimateDistance(to, start) + estimateDistance(to, end);
const startCost = isEqualPoint(from, start)
? areLinesSameDirection(from, to, source, start)
? -base / 2
: 0
: 0;
const endCost = isEqualPoint(to, end)
? areLinesSameDirection(from, to, end, target)
? -base / 2
: 0
: 0;
return base + startCost + endCost;
};
/**
* Calculate the estimated distance between two points
*
* Manhattan distance: the sum of horizontal and vertical distances, faster calculation speed
*/
const estimateDistance = (p1: ControlPoint, p2: ControlPoint): number =>
Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);

View File

@ -1,127 +0,0 @@
import { areLinesSameDirection, isHorizontalFromPosition } from "../edge";
import {
ControlPoint,
HandlePosition,
NodeRect,
getCenterPoints,
getExpandedRect,
getOffsetPoint,
getSidesFromPoints,
getVerticesFromRectVertex,
optimizeInputPoints,
reducePoints,
} from "../point";
import { getAStarPath } from "./a-star";
import { getSimplePath } from "./simple";
export interface GetControlPointsParams {
source: HandlePosition;
target: HandlePosition;
sourceRect: NodeRect;
targetRect: NodeRect;
/**
* Minimum spacing between edges and nodes
*/
offset: number;
}
/**
* Calculate control points on the optimal path of an edge.
*
* Reference article: https://juejin.cn/post/6942727734518874142
*/
export const getControlPoints = ({
source: oldSource,
target: oldTarget,
sourceRect,
targetRect,
offset = 20,
}: GetControlPointsParams) => {
const source: ControlPoint = oldSource;
const target: ControlPoint = oldTarget;
let edgePoints: ControlPoint[] = [];
let optimized: ReturnType<typeof optimizeInputPoints>;
// 1. Find the starting and ending points after applying the offset
const sourceOffset = getOffsetPoint(oldSource, offset);
const targetOffset = getOffsetPoint(oldTarget, offset);
const expandedSource = getExpandedRect(sourceRect, offset);
const expandedTarget = getExpandedRect(targetRect, offset);
// 2. Determine if the two Rects are relatively close or should directly connected
const minOffset = 2 * offset + 10;
const isHorizontalLayout = isHorizontalFromPosition(oldSource.position);
const isSameDirection = areLinesSameDirection(
source,
sourceOffset,
targetOffset,
target
);
const sides = getSidesFromPoints([
source,
target,
sourceOffset,
targetOffset,
]);
const isTooClose = isHorizontalLayout
? sides.right - sides.left < minOffset
: sides.bottom - sides.top < minOffset;
const isDirectConnect = isHorizontalLayout
? isSameDirection && source.x < target.x
: isSameDirection && source.y < target.y;
if (isTooClose || isDirectConnect) {
// 3. If the two Rects are relatively close or directly connected, return a simple Path
edgePoints = getSimplePath({
source,
target,
sourceOffset,
targetOffset,
isDirectConnect,
});
optimized = optimizeInputPoints({
source: oldSource,
target: oldTarget,
sourceOffset,
targetOffset,
edgePoints,
});
edgePoints = optimized.edgePoints;
} else {
// 3. Find the vertices of the two expanded Rects
edgePoints = [
...getVerticesFromRectVertex(expandedSource, targetOffset),
...getVerticesFromRectVertex(expandedTarget, sourceOffset),
];
// 4. Find possible midpoints and intersections
edgePoints = edgePoints.concat(
getCenterPoints({
source: expandedSource,
target: expandedTarget,
sourceOffset,
targetOffset,
})
);
// 5. Merge nearby coordinate points and remove duplicate coordinate points
optimized = optimizeInputPoints({
source: oldSource,
target: oldTarget,
sourceOffset,
targetOffset,
edgePoints,
});
// 6. Find the optimal path
edgePoints = getAStarPath({
points: optimized.edgePoints,
source: optimized.source,
target: optimized.target,
sourceRect: getExpandedRect(sourceRect, offset / 2),
targetRect: getExpandedRect(targetRect, offset / 2),
});
}
return {
points: reducePoints([optimized.source, ...edgePoints, optimized.target]),
inputPoints: optimized.edgePoints,
};
};

View File

@ -1,113 +0,0 @@
import { uuid } from "@/utils/uuid";
import { LayoutDirection } from "../../node";
import { ControlPoint, isInLine, isOnLine } from "../point";
interface GetSimplePathParams {
isDirectConnect?: boolean;
source: ControlPoint;
target: ControlPoint;
sourceOffset: ControlPoint;
targetOffset: ControlPoint;
}
const getLineDirection = (
start: ControlPoint,
end: ControlPoint
): LayoutDirection => (start.x === end.x ? "vertical" : "horizontal");
/**
* When two nodes are too close, use the simple path
*
* @returns Control points including sourceOffset and targetOffset (not including source and target points).
*/
export const getSimplePath = ({
isDirectConnect,
source,
target,
sourceOffset,
targetOffset,
}: GetSimplePathParams): ControlPoint[] => {
const points: ControlPoint[] = [];
const sourceDirection = getLineDirection(source, sourceOffset);
const targetDirection = getLineDirection(target, targetOffset);
const isHorizontalLayout = sourceDirection === "horizontal";
if (isDirectConnect) {
// Direct connection, return a simple Path
if (isHorizontalLayout) {
if (sourceOffset.x <= targetOffset.x) {
const centerX = (sourceOffset.x + targetOffset.x) / 2;
return [
{ id: uuid(), x: centerX, y: sourceOffset.y },
{ id: uuid(), x: centerX, y: targetOffset.y },
];
} else {
const centerY = (sourceOffset.y + targetOffset.y) / 2;
return [
sourceOffset,
{ id: uuid(), x: sourceOffset.x, y: centerY },
{ id: uuid(), x: targetOffset.x, y: centerY },
targetOffset,
];
}
} else {
if (sourceOffset.y <= targetOffset.y) {
const centerY = (sourceOffset.y + targetOffset.y) / 2;
return [
{ id: uuid(), x: sourceOffset.x, y: centerY },
{ id: uuid(), x: targetOffset.x, y: centerY },
];
} else {
const centerX = (sourceOffset.x + targetOffset.x) / 2;
return [
sourceOffset,
{ id: uuid(), x: centerX, y: sourceOffset.y },
{ id: uuid(), x: centerX, y: targetOffset.y },
targetOffset,
];
}
}
}
if (sourceDirection === targetDirection) {
// Same direction, add two points, two endpoints of parallel lines at half the vertical distance
if (source.y === sourceOffset.y) {
points.push({
id: uuid(),
x: sourceOffset.x,
y: (sourceOffset.y + targetOffset.y) / 2,
});
points.push({
id: uuid(),
x: targetOffset.x,
y: (sourceOffset.y + targetOffset.y) / 2,
});
} else {
points.push({
id: uuid(),
x: (sourceOffset.x + targetOffset.x) / 2,
y: sourceOffset.y,
});
points.push({
id: uuid(),
x: (sourceOffset.x + targetOffset.x) / 2,
y: targetOffset.y,
});
}
} else {
// Different directions, add one point, ensure it's not on the current line segment (to avoid overlap), and there are no turns
let point = { id: uuid(), x: sourceOffset.x, y: targetOffset.y };
const inStart = isInLine(point, source, sourceOffset);
const inEnd = isInLine(point, target, targetOffset);
if (inStart || inEnd) {
point = { id: uuid(), x: targetOffset.x, y: sourceOffset.y };
} else {
const onStart = isOnLine(point, source, sourceOffset);
const onEnd = isOnLine(point, target, targetOffset);
if (onStart && onEnd) {
point = { id: uuid(), x: targetOffset.x, y: sourceOffset.y };
}
}
points.push(point);
}
return [sourceOffset, ...points, targetOffset];
};

View File

@ -1,26 +0,0 @@
import { LayoutOptions, LayoutStrategy, NodeWithLayout } from "./types";
import { Edge, Node } from "@xyflow/react";
// 抽象布局类,包含共用的工具方法
export abstract class BaseLayout implements LayoutStrategy {
protected buildNodeMap(nodes: Node[]): Map<string, NodeWithLayout> {
const nodeMap = new Map<string, NodeWithLayout>();
nodes.forEach(node => {
nodeMap.set(node.id, { ...node, children: [], width: 150, height: 40 });
});
return nodeMap;
}
protected buildTreeStructure(nodeMap: Map<string, NodeWithLayout>, edges: Edge[]): NodeWithLayout | undefined {
edges.forEach(edge => {
const source = nodeMap.get(edge.source);
const target = nodeMap.get(edge.target);
if (source && target) {
source.children?.push(target);
target.parent = source;
}
});
return Array.from(nodeMap.values()).find(node => !node.parent);
}
abstract layout(options: LayoutOptions): { nodes: Node[], edges: Edge[] };
}

View File

@ -1,87 +0,0 @@
import { Edge,Node } from "@xyflow/react";
import { BaseLayout } from "./BaseLayout";
import { LayoutOptions, NodeWithLayout } from "./types";
// 思维导图布局实现
export class MindMapLayout extends BaseLayout {
layout(options: LayoutOptions): { nodes: Node[], edges: Edge[] } {
const {
nodes,
edges,
levelSeparation = 200,
nodeSeparation = 60
} = options;
const nodeMap = this.buildNodeMap(nodes);
const rootNode = this.buildTreeStructure(nodeMap, edges);
if (!rootNode) return { nodes, edges };
this.assignSides(rootNode);
this.calculateSubtreeHeight(rootNode, nodeSeparation);
this.calculateLayout(rootNode, 0, 0, levelSeparation, nodeSeparation);
const layoutedNodes = Array.from(nodeMap.values()).map(node => ({
...node,
position: node.position,
}));
return { nodes: layoutedNodes, edges };
}
private assignSides(node: NodeWithLayout, isRight: boolean = true): void {
if (!node.children?.length) return;
const len = node.children.length;
const midIndex = Math.floor(len / 2);
if (!node.parent) {
for (let i = 0; i < len; i++) {
const child = node.children[i];
this.assignSides(child, i < midIndex);
child.isRight = i < midIndex;
}
} else {
node.children.forEach(child => {
this.assignSides(child, isRight);
child.isRight = isRight;
});
}
}
private calculateSubtreeHeight(node: NodeWithLayout, nodeSeparation: number): number {
if (!node.children?.length) {
node.subtreeHeight = node.height || 40;
return node.subtreeHeight;
}
const childrenHeight = node.children.reduce((sum, child) => {
return sum + this.calculateSubtreeHeight(child, nodeSeparation);
}, 0);
const totalGaps = (node.children.length - 1) * nodeSeparation;
node.subtreeHeight = Math.max(node.height || 40, childrenHeight + totalGaps);
return node.subtreeHeight;
}
private calculateLayout(
node: NodeWithLayout,
x: number,
y: number,
levelSeparation: number,
nodeSeparation: number
): void {
node.position = { x, y };
if (!node.children?.length) return;
let currentY = y - (node.subtreeHeight || 0) / 2;
node.children.forEach(child => {
const direction = child.isRight ? 1 : -1;
const childX = x + (levelSeparation * direction);
const childY = currentY + (child.subtreeHeight || 0) / 2;
this.calculateLayout(child, childX, childY, levelSeparation, nodeSeparation);
currentY += (child.subtreeHeight || 0) + nodeSeparation;
});
}
}

View File

@ -1,127 +0,0 @@
import { Edge, Node } from "@xyflow/react";
import { BaseLayout } from "./BaseLayout";
import { LayoutOptions, NodeWithLayout } from "./types";
/**
* SingleMapLayout BaseLayout
* 使
*/
export class SingleMapLayout extends BaseLayout {
/**
*
* @param options
* @returns
*/
layout(options: LayoutOptions): { nodes: Node[], edges: Edge[] } {
const { nodes, edges, levelSeparation = 100, nodeSeparation = 30 } = options;
const nodeMap = this.buildNodeMap(nodes);
const root = this.buildTreeStructure(nodeMap, edges);
if (!root) {
return { nodes: [], edges: [] };
}
// 计算子树的尺寸
this.calculateSubtreeDimensions(root);
// 第一遍:分配垂直位置
this.assignInitialVerticalPositions(root, 0);
// 第二遍:使用平衡布局定位节点
this.positionNodes(root, 0, 0, levelSeparation, nodeSeparation, 'right');
return {
nodes: Array.from(nodeMap.values()),
edges
};
}
/**
*
* @param node
*/
private calculateSubtreeDimensions(node: NodeWithLayout): void {
node.subtreeHeight = node.height || 40;
node.subtreeWidth = node.width || 150;
if (node.children && node.children.length > 0) {
// 首先计算所有子节点的尺寸
node.children.forEach(child => this.calculateSubtreeDimensions(child));
// 计算子节点所需的总高度,包括间距
const totalChildrenHeight = this.calculateTotalChildrenHeight(node.children, 30);
// 更新节点的子树尺寸
node.subtreeHeight = Math.max(node.subtreeHeight, totalChildrenHeight);
node.subtreeWidth += Math.max(...node.children.map(child => child.subtreeWidth || 0));
}
}
/**
*
* @param children
* @param spacing
* @returns
*/
private calculateTotalChildrenHeight(children: NodeWithLayout[], spacing: number): number {
if (!children.length) return 0;
const totalHeight = children.reduce((sum, child) => sum + (child.subtreeHeight || 0), 0);
return totalHeight + (spacing * (children.length - 1));
}
/**
*
* @param node
* @param level
*/
private assignInitialVerticalPositions(node: NodeWithLayout, level: number): void {
if (!node.children?.length) return;
const totalHeight = this.calculateTotalChildrenHeight(node.children, 30);
let currentY = -(totalHeight / 2);
node.children.forEach(child => {
const childHeight = child.subtreeHeight || 0;
child.verticalLevel = level + 1;
child.relativeY = currentY + (childHeight / 2);
this.assignInitialVerticalPositions(child, level + 1);
currentY += childHeight + 30; // 30 是垂直间距
});
}
/**
*
* @param node
* @param x
* @param y
* @param levelSeparation
* @param nodeSeparation
* @param direction 'left' 'right'
*/
private positionNodes(
node: NodeWithLayout,
x: number,
y: number,
levelSeparation: number,
nodeSeparation: number,
direction: 'left' | 'right'
): void {
node.position = { x, y };
if (!node.children?.length) return;
// 计算子节点的水平位置
const nextX = direction === 'right'
? x + (node.width || 0) + levelSeparation
: x - (node.width || 0) - levelSeparation;
// 定位每个子节点
node.children.forEach(child => {
const childY = y + (child.relativeY || 0);
this.positionNodes(
child,
nextX,
childY,
levelSeparation,
nodeSeparation,
direction
);
});
}
}

View File

@ -1,68 +0,0 @@
import { BaseLayout } from "./BaseLayout";
import { LayoutOptions, LayoutStrategy, NodeWithLayout } from "./types";
import { Edge, Node } from "@xyflow/react";
export class TreeLayout extends BaseLayout {
layout(options: LayoutOptions): { nodes: Node[], edges: Edge[] } {
const {
nodes,
edges,
levelSeparation = 100, // 层级间垂直距离
nodeSeparation = 50 // 节点间水平距离
} = options;
const nodeMap = this.buildNodeMap(nodes);
const rootNode = this.buildTreeStructure(nodeMap, edges);
if (!rootNode) return { nodes, edges };
// 计算每个节点的子树宽度
this.calculateSubtreeWidth(rootNode, nodeSeparation);
// 计算布局位置
this.calculateTreeLayout(rootNode, 0, 0, levelSeparation);
const layoutedNodes = Array.from(nodeMap.values()).map(node => ({
...node,
position: node.position,
}));
return { nodes: layoutedNodes, edges };
}
private calculateSubtreeWidth(node: NodeWithLayout, nodeSeparation: number): number {
if (!node.children?.length) {
node.subtreeWidth = node.width || 150;
return node.subtreeWidth;
}
const childrenWidth = node.children.reduce((sum, child) => {
return sum + this.calculateSubtreeWidth(child, nodeSeparation);
}, 0);
const totalGaps = (node.children.length - 1) * nodeSeparation;
node.subtreeWidth = Math.max(node.width || 150, childrenWidth + totalGaps);
return node.subtreeWidth;
}
private calculateTreeLayout(
node: NodeWithLayout,
x: number,
y: number,
levelSeparation: number
): void {
node.position = { x, y };
if (!node.children?.length) return;
const totalChildrenWidth = node.children.reduce((sum, child) =>
sum + (child.subtreeWidth || 0), 0);
const totalGaps = (node.children.length - 1) * (node.width || 150);
// 计算最左侧子节点的起始x坐标
let startX = x - (totalChildrenWidth + totalGaps) / 2;
node.children.forEach(child => {
const childX = startX + (child.subtreeWidth || 0) / 2;
const childY = y + levelSeparation;
this.calculateTreeLayout(child, childX, childY, levelSeparation);
startX += (child.subtreeWidth || 0) + (node.width || 150);
});
}
}

View File

@ -1,248 +0,0 @@
import { areLinesReverseDirection, areLinesSameDirection } from "../edge";
import {
ControlPoint,
NodeRect,
isEqualPoint,
isSegmentCrossingRect,
} from "../point";
interface GetAStarPathParams {
/**
* Collection of potential control points between `sourceOffset` and `targetOffset`, excluding the `source` and `target` points.
*/
points: ControlPoint[];
source: ControlPoint;
target: ControlPoint;
/**
* Node size information for the `source` and `target`, used to optimize edge routing without intersecting nodes.
*/
sourceRect: NodeRect;
targetRect: NodeRect;
}
/**
* Utilizes the [A\* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm) combined with
* [Manhattan Distance](https://simple.wikipedia.org/wiki/Manhattan_distance) to find the optimal path for edges.
*
* @returns Control points including sourceOffset and targetOffset (not including source and target points).
*/
export const getAStarPath = ({
points,
source,
target,
sourceRect,
targetRect,
}: GetAStarPathParams): ControlPoint[] => {
if (points.length < 3) {
return points;
}
const start = points[0];
const end = points[points.length - 1];
const openSet: ControlPoint[] = [start];
const closedSet: Set<ControlPoint> = new Set();
const cameFrom: Map<ControlPoint, ControlPoint> = new Map();
const gScore: Map<ControlPoint, number> = new Map().set(start, 0);
const fScore: Map<ControlPoint, number> = new Map().set(
start,
heuristicCostEstimate({
from: start,
to: start,
start,
end,
source,
target,
})
);
while (openSet.length) {
let current;
let currentIdx;
let lowestFScore = Infinity;
openSet.forEach((p, idx) => {
const score = fScore.get(p) ?? 0;
if (score < lowestFScore) {
lowestFScore = score;
current = p;
currentIdx = idx;
}
});
if (!current) {
break;
}
if (current === end) {
return buildPath(cameFrom, current);
}
openSet.splice(currentIdx!, 1);
closedSet.add(current);
const curFScore = fScore.get(current) ?? 0;
const previous = cameFrom.get(current);
const neighbors = getNextNeighborPoints({
points,
previous,
current,
sourceRect,
targetRect,
});
for (const neighbor of neighbors) {
if (closedSet.has(neighbor)) {
continue;
}
const neighborGScore = gScore.get(neighbor) ?? 0;
const tentativeGScore = curFScore + estimateDistance(current, neighbor);
if (openSet.includes(neighbor) && tentativeGScore >= neighborGScore) {
continue;
}
openSet.push(neighbor);
cameFrom.set(neighbor, current);
gScore.set(neighbor, tentativeGScore);
fScore.set(
neighbor,
neighborGScore +
heuristicCostEstimate({
from: current,
to: neighbor,
start,
end,
source,
target,
})
);
}
}
return [start, end];
};
const buildPath = (
cameFrom: Map<ControlPoint, ControlPoint>,
current: ControlPoint
): ControlPoint[] => {
const path = [current];
let previous = cameFrom.get(current);
while (previous) {
path.push(previous);
previous = cameFrom.get(previous);
}
return path.reverse();
};
interface GetNextNeighborPointsParams {
points: ControlPoint[];
previous?: ControlPoint;
current: ControlPoint;
sourceRect: NodeRect;
targetRect: NodeRect;
}
/**
* Get the set of possible neighboring points for the current control point
*
* - The line is in a horizontal or vertical direction
* - The line does not intersect with the two end nodes
* - The line does not overlap with the previous line segment in reverse direction
*/
export const getNextNeighborPoints = ({
points,
previous,
current,
sourceRect,
targetRect,
}: GetNextNeighborPointsParams): ControlPoint[] => {
return points.filter((p) => {
if (p === current) {
return false;
}
// The connection is in the horizontal or vertical direction
const rightDirection = p.x === current.x || p.y === current.y;
// Reverse direction with the previous line segment (overlap)
const reverseDirection = previous
? areLinesReverseDirection(previous, current, current, p)
: false;
return (
rightDirection && // The line is in a horizontal or vertical direction
!reverseDirection && // The line does not overlap with the previous line segment in reverse direction
!isSegmentCrossingRect(p, current, sourceRect) && // Does not intersect with sourceNode
!isSegmentCrossingRect(p, current, targetRect) // Does not intersect with targetNode
);
});
};
/**
*
* :
* - from/to: 当前路径段的起点和终点
* - start/end: 整条路径的起点和终点
* - source/target: 连接的源节点和目标节点位置
*/
interface HeuristicCostParams {
from: ControlPoint; // 当前路径段的起点
to: ControlPoint; // 当前路径段的终点
start: ControlPoint; // 整条路径的起始点
end: ControlPoint; // 整条路径的终点
source: ControlPoint; // 源节点的连接点
target: ControlPoint; // 目标节点的连接点
}
/**
*
*
* :
* 1. 基础代价: 当前点到起点和终点的曼哈顿距离之和
* 2. 起点优化: 如果是起始段,
* 3. 终点优化: 如果是结束段,
*
* :
* -
* -
* - 使
*
* @param params
* @returns ,
*/
const heuristicCostEstimate = ({
from,
to,
start,
end,
source,
target,
}: HeuristicCostParams): number => {
// 计算基础代价 - 到起点和终点的距离之和
const base = estimateDistance(to, start) + estimateDistance(to, end);
// 起点方向优化 - 如果是起始段且方向一致,给予奖励
const startCost = isEqualPoint(from, start)
? areLinesSameDirection(from, to, source, start)
? -base / 2 // 方向一致时减少代价
: 0
: 0;
// 终点方向优化 - 如果是结束段且方向一致,给予奖励
const endCost = isEqualPoint(to, end)
? areLinesSameDirection(from, to, end, target)
? -base / 2 // 方向一致时减少代价
: 0
: 0;
return base + startCost + endCost;
};
/**
*
*
* (Manhattan distance):
* -
* - 使
* -
*
* @param p1
* @param p2
* @returns
*/
const estimateDistance = (p1: ControlPoint, p2: ControlPoint): number =>
Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);

View File

@ -1,142 +0,0 @@
import { areLinesSameDirection, isHorizontalFromPosition } from "../edge";
import {
ControlPoint,
HandlePosition,
NodeRect,
getCenterPoints,
getExpandedRect,
getOffsetPoint,
getSidesFromPoints,
getVerticesFromRectVertex,
optimizeInputPoints,
reducePoints,
} from "../point";
import { getAStarPath } from "./a-star";
import { getSimplePath } from "./simple";
/**
*
* 线,
* 线
*/
/**
*
*/
export interface GetControlPointsParams {
source: HandlePosition; // 起始连接点位置
target: HandlePosition; // 目标连接点位置
sourceRect: NodeRect; // 起始节点的矩形区域
targetRect: NodeRect; // 目标节点的矩形区域
/**
*
* @default 20
*/
offset: number;
}
/**
* 线
* @param params
* @returns
*/
export const getControlPoints = ({
source: oldSource,
target: oldTarget,
sourceRect,
targetRect,
offset = 20,
}: GetControlPointsParams) => {
const source: ControlPoint = oldSource;
const target: ControlPoint = oldTarget;
let edgePoints: ControlPoint[] = [];
let optimized: ReturnType<typeof optimizeInputPoints>;
// 1. 计算考虑偏移量后的起始和结束点
const sourceOffset = getOffsetPoint(oldSource, offset);
const targetOffset = getOffsetPoint(oldTarget, offset);
const expandedSource = getExpandedRect(sourceRect, offset);
const expandedTarget = getExpandedRect(targetRect, offset);
// 2. 判断两个矩形是否靠得较近或应该直接连接
const minOffset = 2 * offset + 10; // 最小间距阈值
const isHorizontalLayout = isHorizontalFromPosition(oldSource.position); // 是否为水平布局
const isSameDirection = areLinesSameDirection(
source,
sourceOffset,
targetOffset,
target
); // 判断是否同向
const sides = getSidesFromPoints([
source,
target,
sourceOffset,
targetOffset,
]); // 获取边界信息
// 判断节点是否过近
const isTooClose = isHorizontalLayout
? sides.right - sides.left < minOffset
: sides.bottom - sides.top < minOffset;
// 判断是否可以直接连接
const isDirectConnect = isHorizontalLayout
? isSameDirection && source.x < target.x
: isSameDirection && source.y < target.y;
if (isTooClose || isDirectConnect) {
// 3. 如果节点较近或可直接连接,返回简单路径
edgePoints = getSimplePath({
source,
target,
sourceOffset,
targetOffset,
isDirectConnect,
});
// 优化输入点
optimized = optimizeInputPoints({
source: oldSource,
target: oldTarget,
sourceOffset,
targetOffset,
edgePoints,
});
edgePoints = optimized.edgePoints;
} else {
// 3. 获取两个扩展矩形的顶点
edgePoints = [
...getVerticesFromRectVertex(expandedSource, targetOffset),
...getVerticesFromRectVertex(expandedTarget, sourceOffset),
];
// 4. 计算可能的中点和交点
edgePoints = edgePoints.concat(
getCenterPoints({
source: expandedSource,
target: expandedTarget,
sourceOffset,
targetOffset,
})
);
// 5. 合并临近坐标点并去除重复点
optimized = optimizeInputPoints({
source: oldSource,
target: oldTarget,
sourceOffset,
targetOffset,
edgePoints,
});
// 6. 使用A*算法寻找最优路径
edgePoints = getAStarPath({
points: optimized.edgePoints,
source: optimized.source,
target: optimized.target,
sourceRect: getExpandedRect(sourceRect, offset / 2),
targetRect: getExpandedRect(targetRect, offset / 2),
});
}
// 返回简化后的路径点和输入点集合
return {
points: reducePoints([optimized.source, ...edgePoints, optimized.target]),
inputPoints: optimized.edgePoints,
};
};

View File

@ -1,112 +0,0 @@
import { uuid } from "../../../utils/uuid";
import { LayoutDirection } from "../../node";
import { ControlPoint, isInLine, isOnLine } from "../point";
interface GetSimplePathParams {
isDirectConnect?: boolean;
source: ControlPoint;
target: ControlPoint;
sourceOffset: ControlPoint;
targetOffset: ControlPoint;
}
const getLineDirection = (
start: ControlPoint,
end: ControlPoint
): LayoutDirection => (start.x === end.x ? "vertical" : "horizontal");
/**
* When two nodes are too close, use the simple path
*
* @returns Control points including sourceOffset and targetOffset (not including source and target points).
*/
export const getSimplePath = ({
isDirectConnect,
source,
target,
sourceOffset,
targetOffset,
}: GetSimplePathParams): ControlPoint[] => {
const points: ControlPoint[] = [];
const sourceDirection = getLineDirection(source, sourceOffset);
const targetDirection = getLineDirection(target, targetOffset);
const isHorizontalLayout = sourceDirection === "horizontal";
if (isDirectConnect) {
// Direct connection, return a simple Path
if (isHorizontalLayout) {
if (sourceOffset.x <= targetOffset.x) {
const centerX = (sourceOffset.x + targetOffset.x) / 2;
return [
{ id: uuid(), x: centerX, y: sourceOffset.y },
{ id: uuid(), x: centerX, y: targetOffset.y },
];
} else {
const centerY = (sourceOffset.y + targetOffset.y) / 2;
return [
sourceOffset,
{ id: uuid(), x: sourceOffset.x, y: centerY },
{ id: uuid(), x: targetOffset.x, y: centerY },
targetOffset,
];
}
} else {
if (sourceOffset.y <= targetOffset.y) {
const centerY = (sourceOffset.y + targetOffset.y) / 2;
return [
{ id: uuid(), x: sourceOffset.x, y: centerY },
{ id: uuid(), x: targetOffset.x, y: centerY },
];
} else {
const centerX = (sourceOffset.x + targetOffset.x) / 2;
return [
sourceOffset,
{ id: uuid(), x: centerX, y: sourceOffset.y },
{ id: uuid(), x: centerX, y: targetOffset.y },
targetOffset,
];
}
}
}
if (sourceDirection === targetDirection) {
// Same direction, add two points, two endpoints of parallel lines at half the vertical distance
if (source.y === sourceOffset.y) {
points.push({
id: uuid(),
x: sourceOffset.x,
y: (sourceOffset.y + targetOffset.y) / 2,
});
points.push({
id: uuid(),
x: targetOffset.x,
y: (sourceOffset.y + targetOffset.y) / 2,
});
} else {
points.push({
id: uuid(),
x: (sourceOffset.x + targetOffset.x) / 2,
y: sourceOffset.y,
});
points.push({
id: uuid(),
x: (sourceOffset.x + targetOffset.x) / 2,
y: targetOffset.y,
});
}
} else {
// Different directions, add one point, ensure it's not on the current line segment (to avoid overlap), and there are no turns
let point = { id: uuid(), x: sourceOffset.x, y: targetOffset.y };
const inStart = isInLine(point, source, sourceOffset);
const inEnd = isInLine(point, target, targetOffset);
if (inStart || inEnd) {
point = { id: uuid(), x: targetOffset.x, y: sourceOffset.y };
} else {
const onStart = isOnLine(point, source, sourceOffset);
const onEnd = isOnLine(point, target, targetOffset);
if (onStart && onEnd) {
point = { id: uuid(), x: targetOffset.x, y: sourceOffset.y };
}
}
points.push(point);
}
return [sourceOffset, ...points, targetOffset];
};

View File

@ -1,389 +0,0 @@
import { Position, XYPosition } from "@xyflow/react";
import { ControlPoint, HandlePosition } from "./point";
import { uuid } from "../../utils/uuid";
export interface ILine {
start: ControlPoint;
end: ControlPoint;
}
/**
*
* @param position -
* @returns true,false
*/
export const isHorizontalFromPosition = (position: Position) => {
return [Position.Left, Position.Right].includes(position);
};
/**
*
* ,线线,
* @param props -
* @param props.source -
* @param props.target -
* @returns true,false
*/
export const isConnectionBackward = (props: {
source: HandlePosition;
target: HandlePosition;
}) => {
const { source, target } = props;
// 判断是水平还是垂直方向的连接
const isHorizontal = isHorizontalFromPosition(source.position);
let isBackward = false;
// 水平方向时,如果源点x坐标大于目标点,则为反向
if (isHorizontal) {
if (source.x > target.x) {
isBackward = true;
}
}
// 垂直方向时,如果源点y坐标大于目标点,则为反向
else {
if (source.y > target.y) {
isBackward = true;
}
}
return isBackward;
};
/**
*
* 使(Math.hypot)线
* @param p1 -
* @param p2 -
* @returns
*/
export const distance = (p1: ControlPoint, p2: ControlPoint) => {
return Math.hypot(p2.x - p1.x, p2.y - p1.y);
};
/**
* 线
*
* @param p1 -
* @param p2 -
* @returns
*/
export const getLineCenter = (
p1: ControlPoint,
p2: ControlPoint
): ControlPoint => {
return {
id: uuid(),
x: (p1.x + p2.x) / 2, // x坐标取两端点x坐标的平均值
y: (p1.y + p2.y) / 2, // y坐标取两端点y坐标的平均值
};
};
/**
* 线
*
* @description
* 线
* :
* 1. 垂直线段: 当起点和终点的x坐标相同时,x坐标是否等于线段x坐标,y坐标在线段y坐标范围内
* 2. 水平线段: 当起点和终点的y坐标相同时,y坐标是否等于线段y坐标,x坐标在线段x坐标范围内
*
* @param start - 线
* @param end - 线
* @param p -
* @returns {boolean} 线true,false
*/
export const isLineContainsPoint = (
start: ControlPoint,
end: ControlPoint,
p: ControlPoint
) => {
return (
// 判断垂直线段
(start.x === end.x && // 起点终点x坐标相同
p.x === start.x && // 目标点x坐标与线段相同
p.y <= Math.max(start.y, end.y) && // 目标点y坐标不超过线段y坐标最大值
p.y >= Math.min(start.y, end.y)) || // 目标点y坐标不小于线段y坐标最小值
// 判断水平线段
(start.y === end.y && // 起点终点y坐标相同
p.y === start.y && // 目标点y坐标与线段相同
p.x <= Math.max(start.x, end.x) && // 目标点x坐标不超过线段x坐标最大值
p.x >= Math.min(start.x, end.x)) // 目标点x坐标不小于线段x坐标最小值
);
};
/**
/**
* SVG路径
*
* 线:
* 1. 线
* 2.
* 3.
*
* @param points ,
* - 2()
* -
* @param radius
* @returns SVG路径字符串
* @throws points数组长度小于2时抛出错误
*/
export function getPathWithRoundCorners(
points: ControlPoint[],
radius: number
): string {
if (points.length < 2) {
throw new Error("At least 2 points are required.");
}
/**
* 线
* @param center
* @param p1
* @param p2
* @param radius
* @returns SVG路径命令字符串
*/
function getRoundCorner(
center: ControlPoint,
p1: ControlPoint,
p2: ControlPoint,
radius: number
) {
const { x, y } = center;
// 如果两条线段不垂直,则直接返回直线路径
if (!areLinesPerpendicular(p1, center, center, p2)) {
return `L ${x} ${y}`;
}
// 计算实际可用的圆角半径,取三个值中的最小值:
// 1. 与前一个点的距离的一半
// 2. 与后一个点的距离的一半
// 3. 传入的目标半径
const d1 = distance(center, p1);
const d2 = distance(center, p2);
radius = Math.min(d1 / 2, d2 / 2, radius);
// 判断第一条线段是否为水平线
const isHorizontal = p1.y === y;
// 根据点的相对位置确定圆角绘制方向
const xDir = isHorizontal ? (p1.x < p2.x ? -1 : 1) : p1.x < p2.x ? 1 : -1;
const yDir = isHorizontal ? (p1.y < p2.y ? 1 : -1) : p1.y < p2.y ? -1 : 1;
// 根据线段方向生成不同的圆角路径
if (isHorizontal) {
return `L ${x + radius * xDir},${y}Q ${x},${y} ${x},${y + radius * yDir}`;
}
return `L ${x},${y + radius * yDir}Q ${x},${y} ${x + radius * xDir},${y}`;
}
// 构建完整的SVG路径
const path: string[] = [];
for (let i = 0; i < points.length; i++) {
if (i === 0) {
// 起点使用移动命令M
path.push(`M ${points[i].x} ${points[i].y}`);
} else if (i === points.length - 1) {
// 终点使用直线命令L
path.push(`L ${points[i].x} ${points[i].y}`);
} else {
// 中间点使用圆角转角
path.push(
getRoundCorner(points[i], points[i - 1], points[i + 1], radius)
);
}
}
// 将所有路径命令连接成完整的路径字符串
return path.join(" ");
}
/**
* 线线
* @param points x和y坐标
* @returns 线
*
* :
* 1. 线线
* 2. ,线
* 3. 线,线
* 4. 线
*/
export function getLongestLine(
points: ControlPoint[]
): [ControlPoint, ControlPoint] {
let longestLine: [ControlPoint, ControlPoint] = [points[0], points[1]];
let longestDistance = distance(...longestLine);
for (let i = 1; i < points.length - 1; i++) {
const _distance = distance(points[i], points[i + 1]);
if (_distance > longestDistance) {
longestDistance = _distance;
longestLine = [points[i], points[i + 1]];
}
}
return longestLine;
}
/**
* 线
* @param points
* @param minGap ,20
* @returns
*
* :
* 1. 线:
* -
* - ,
* 2. 线:
* - 线
* - 线
*/
export function getLabelPosition(
points: ControlPoint[],
minGap = 20
): XYPosition {
if (points.length % 2 === 0) {
const middleP1 = points[points.length / 2 - 1];
const middleP2 = points[points.length / 2];
if (distance(middleP1, middleP2) > minGap) {
return getLineCenter(middleP1, middleP2);
}
}
const [start, end] = getLongestLine(points);
return {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2,
};
}
/**
* 线
* @param p1,p2 线
* @param p3,p4 线
* @returns 线true
*
* :
* - 线
* - 线(y相等)(x相等),线
*/
export function areLinesPerpendicular(
p1: ControlPoint,
p2: ControlPoint,
p3: ControlPoint,
p4: ControlPoint
): boolean {
return (p1.x === p2.x && p3.y === p4.y) || (p1.y === p2.y && p3.x === p4.x);
}
/**
* 线
* @param p1,p2 线
* @param p3,p4 线
* @returns 线true
*
* :
* - 线
* - 线(x相等)(y相等),线
*/
export function areLinesParallel(
p1: ControlPoint,
p2: ControlPoint,
p3: ControlPoint,
p4: ControlPoint
) {
return (p1.x === p2.x && p3.x === p4.x) || (p1.y === p2.y && p3.y === p4.y);
}
/**
* 线
* @param p1 线
* @param p2 线
* @param p3 线
* @param p4 线
* @returns boolean 线true,false
*
* :
* 1. 线(y坐标相等),x方向的变化是否同向
* 2. 线(x坐标相等),y方向的变化是否同向
*/
export function areLinesSameDirection(
p1: ControlPoint,
p2: ControlPoint,
p3: ControlPoint,
p4: ControlPoint
) {
return (
// 判断垂直线段是否同向
(p1.x === p2.x && p3.x === p4.x && (p1.y - p2.y) * (p3.y - p4.y) > 0) ||
// 判断水平线段是否同向
(p1.y === p2.y && p3.y === p4.y && (p1.x - p2.x) * (p3.x - p4.x) > 0)
);
}
/**
* 线
* @param p1 线
* @param p2 线
* @param p3 线
* @param p4 线
* @returns boolean 线true,false
*
* :
* 1. 线(y坐标相等),x方向的变化是否反向
* 2. 线(x坐标相等),y方向的变化是否反向
*/
export function areLinesReverseDirection(
p1: ControlPoint,
p2: ControlPoint,
p3: ControlPoint,
p4: ControlPoint
) {
return (
// 判断垂直线段是否反向
(p1.x === p2.x && p3.x === p4.x && (p1.y - p2.y) * (p3.y - p4.y) < 0) ||
// 判断水平线段是否反向
(p1.y === p2.y && p3.y === p4.y && (p1.x - p2.x) * (p3.x - p4.x) < 0)
);
}
/**
* 线
* @param p1 线
* @param p2 线
* @param p3 线
* @param p4 线
* @returns number 线(单位:)
*
* :
* 1.
* 2.
* 3.
* 4. 使
* 5.
*/
export function getAngleBetweenLines(
p1: ControlPoint,
p2: ControlPoint,
p3: ControlPoint,
p4: ControlPoint
) {
// 计算两条线段对应的向量
const v1 = { x: p2.x - p1.x, y: p2.y - p1.y };
const v2 = { x: p4.x - p3.x, y: p4.y - p3.y };
// 计算向量的点积
const dotProduct = v1.x * v2.x + v1.y * v2.y;
// 计算两个向量的模长
const magnitude1 = Math.sqrt(v1.x ** 2 + v1.y ** 2);
const magnitude2 = Math.sqrt(v2.x ** 2 + v2.y ** 2);
// 计算夹角的余弦值
const cosine = dotProduct / (magnitude1 * magnitude2);
// 使用反余弦函数计算弧度
const angleInRadians = Math.acos(cosine);
// 将弧度转换为角度并返回
const angleInDegrees = (angleInRadians * 180) / Math.PI;
return angleInDegrees;
}

View File

@ -1,72 +0,0 @@
import { EdgeLayout } from "../../types";
import { getControlPoints, GetControlPointsParams } from "./algorithms";
import { getLabelPosition, getPathWithRoundCorners } from "./edge";
import { InternalNode, Node } from "@xyflow/react"
interface GetBasePathParams extends GetControlPointsParams {
borderRadius: number;
}
export function getBasePath({
id,
offset,
borderRadius,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: any) {
const sourceNode: InternalNode =
kReactFlow.instance!.getNode(source)!;
const targetNode: InternalNode =
kReactFlow.instance!.getNode(target)!;
return getPathWithPoints({
offset,
borderRadius,
source: {
id: "source-" + id,
x: sourceX,
y: sourceY,
position: sourcePosition,
},
target: {
id: "target-" + id,
x: targetX,
y: targetY,
position: targetPosition,
},
sourceRect: {
...(sourceNode.internals.positionAbsolute || sourceNode.position),
width: sourceNode.width!,
height: sourceNode.height!,
},
targetRect: {
...(targetNode.internals.positionAbsolute || targetNode.position),
width: targetNode.width!,
height: targetNode.height!,
},
});
}
export function getPathWithPoints({
source,
target,
sourceRect,
targetRect,
offset = 20,
borderRadius = 16,
}: GetBasePathParams): EdgeLayout {
const { points, inputPoints } = getControlPoints({
source,
target,
offset,
sourceRect,
targetRect,
});
const labelPosition = getLabelPosition(points);
const path = getPathWithRoundCorners(points, borderRadius);
return { path, points, inputPoints, labelPosition };
}

View File

@ -1,623 +0,0 @@
import { Position } from "@xyflow/react";
import { isHorizontalFromPosition } from "./edge";
import { uuid } from "../../utils/uuid";
export interface ControlPoint {
id: string;
x: number;
y: number;
}
export interface NodeRect {
x: number; // left
y: number; // top
width: number;
height: number;
}
export interface RectSides {
top: number;
right: number;
bottom: number;
left: number;
}
export interface HandlePosition extends ControlPoint {
position: Position;
}
export interface GetVerticesParams {
source: NodeRect;
target: NodeRect;
sourceOffset: ControlPoint;
targetOffset: ControlPoint;
}
/**
*
*
* ,
* :
* 1. 线
* 2.
* 3.
*
* :
* 1.
* 2.
* 3.
* 4.
*
* @param {GetVerticesParams} params -
* @param {Rect} params.source - ,xywidthheight
* @param {Rect} params.target -
* @param {Point} params.sourceOffset -
* @param {Point} params.targetOffset -
* @returns {ControlPoint[]} ,ID和坐标
*/
export const getCenterPoints = ({
source,
target,
sourceOffset,
targetOffset,
}: GetVerticesParams): ControlPoint[] => {
// 特殊情况处理:当源点和目标点在同一直线上时,无法构成有效的控制区域
if (sourceOffset.x === targetOffset.x || sourceOffset.y === targetOffset.y) {
return [];
}
// 步骤1: 获取外部边界
// 收集两个节点的所有顶点,用于构建外部最大矩形
const vertices = [...getRectVertices(source), ...getRectVertices(target)];
const outerSides = getSidesFromPoints(vertices);
// 步骤2: 获取内部边界
// 根据偏移点(实际连接点)计算内部矩形的四条边
const { left, right, top, bottom } = getSidesFromPoints([
sourceOffset,
targetOffset,
]);
// 步骤3: 计算中心参考线
const centerX = (left + right) / 2; // 水平中心线
const centerY = (top + bottom) / 2; // 垂直中心线
// 步骤4: 生成候选控制点
// 在内外两个矩形的边上各生成4个控制点,共8个候选点
const points = [
{ id: uuid(), x: centerX, y: top }, // 内矩形-上
{ id: uuid(), x: right, y: centerY }, // 内矩形-右
{ id: uuid(), x: centerX, y: bottom }, // 内矩形-下
{ id: uuid(), x: left, y: centerY }, // 内矩形-左
{ id: uuid(), x: centerX, y: outerSides.top }, // 外矩形-上
{ id: uuid(), x: outerSides.right, y: centerY }, // 外矩形-右
{ id: uuid(), x: centerX, y: outerSides.bottom },// 外矩形-下
{ id: uuid(), x: outerSides.left, y: centerY }, // 外矩形-左
];
// 步骤5: 过滤无效控制点
// 移除落在源节点或目标节点内部的控制点,避免边穿过节点
return points.filter((p) => {
return !isPointInRect(p, source) && !isPointInRect(p, target);
});
};
/**
*
* @param rect
* @param offset
* @returns
*
*
* :
* 1. x和y坐标各向外偏移offset距离
* 2. 2*offset
*/
export const getExpandedRect = (rect: NodeRect, offset: number): NodeRect => {
return {
x: rect.x - offset,
y: rect.y - offset,
width: rect.width + 2 * offset,
height: rect.height + 2 * offset,
};
};
/**
*
* @param rect1
* @param rect2
* @returns ,true表示重叠,false表示不重叠
*
* 使AABB():
* 1. x轴投影是否重叠
* 2. y轴投影是否重叠
*
*/
export const isRectOverLapping = (rect1: NodeRect, rect2: NodeRect) => {
return (
Math.abs(rect1.x - rect2.x) < (rect1.width + rect2.width) / 2 &&
Math.abs(rect1.y - rect2.y) < (rect1.height + rect2.height) / 2
);
};
/**
*
* @param p
* @param box
* @returns ,true表示点在矩形内,false表示点在矩形外
*
* :
* 1. x坐标在矩形左右边界之间
* 2. y坐标在矩形上下边界之间
*/
export const isPointInRect = (p: ControlPoint, box: NodeRect) => {
const sides = getRectSides(box);
return (
p.x >= sides.left &&
p.x <= sides.right &&
p.y >= sides.top &&
p.y <= sides.bottom
);
};
/**
*
*
* 线
*/
/**
*
* @param box
* @param vertex
* @returns
* :
* 1.
* 2.
* 3.
*/
export const getVerticesFromRectVertex = (
box: NodeRect,
vertex: ControlPoint
): ControlPoint[] => {
const points = [vertex, ...getRectVertices(box)];
const { top, right, bottom, left } = getSidesFromPoints(points);
return [
{ id: uuid(), x: left, y: top }, // 左上角顶点
{ id: uuid(), x: right, y: top }, // 右上角顶点
{ id: uuid(), x: right, y: bottom }, // 右下角顶点
{ id: uuid(), x: left, y: bottom }, // 左下角顶点
];
};
/**
*
* @param points
* @returns
* :
* - 使map和Math.min/max计算坐标的最值
*/
export const getSidesFromPoints = (points: ControlPoint[]) => {
const left = Math.min(...points.map((p) => p.x)); // 最左侧x坐标
const right = Math.max(...points.map((p) => p.x)); // 最右侧x坐标
const top = Math.min(...points.map((p) => p.y)); // 最上方y坐标
const bottom = Math.max(...points.map((p) => p.y)); // 最下方y坐标
return { top, right, bottom, left };
};
/**
*
* @param box
* @returns
* :
* - = x坐标
* - = x + width
* - = y坐标
* - = y + height
*/
export const getRectSides = (box: NodeRect): RectSides => {
const { x: left, y: top, width, height } = box;
const right = left + width;
const bottom = top + height;
return { top, right, bottom, left };
};
/**
*
* @param sides
* @returns
* 顶点顺序: 左上 -> -> ->
*/
export const getRectVerticesFromSides = ({
top,
right,
bottom,
left,
}: RectSides): ControlPoint[] => {
return [
{ id: uuid(), x: left, y: top }, // 左上角顶点
{ id: uuid(), x: right, y: top }, // 右上角顶点
{ id: uuid(), x: right, y: bottom }, // 右下角顶点
{ id: uuid(), x: left, y: bottom }, // 左下角顶点
];
};
/**
*
* @param box
* @returns
* :
* 1.
* 2.
*/
export const getRectVertices = (box: NodeRect) => {
const sides = getRectSides(box);
return getRectVerticesFromSides(sides);
};
/**
* ,
* @param boxes , x,y
* @returns
*
* :
* 1. x (left) x (right)
* 2. y (top) y (bottom)
* 3.
*/
export const mergeRects = (...boxes: NodeRect[]): NodeRect => {
// 计算所有矩形的最左边界
const left = Math.min(
...boxes.reduce((pre, e) => [...pre, e.x, e.x + e.width], [] as number[])
);
// 计算所有矩形的最右边界
const right = Math.max(
...boxes.reduce((pre, e) => [...pre, e.x, e.x + e.width], [] as number[])
);
// 计算所有矩形的最上边界
const top = Math.min(
...boxes.reduce((pre, e) => [...pre, e.y, e.y + e.height], [] as number[])
);
// 计算所有矩形的最下边界
const bottom = Math.max(
...boxes.reduce((pre, e) => [...pre, e.y, e.y + e.height], [] as number[])
);
// 返回能包含所有输入矩形的最小矩形
return {
x: left, // 左上角 x 坐标
y: top, // 左上角 y 坐标
width: right - left, // 宽度 = 最右边界 - 最左边界
height: bottom - top, // 高度 = 最下边界 - 最上边界
};
};
/**
*
* @param box - xy坐标和位置类型()
* @param offset -
* @returns id和新的xy坐标
*/
export const getOffsetPoint = (
box: HandlePosition,
offset: number
): ControlPoint => {
// 根据不同的位置类型计算偏移后的坐标
switch (box.position) {
case Position.Top: // 顶部位置,y坐标向上偏移
return {
id: uuid(),
x: box.x,
y: box.y - offset,
};
case Position.Bottom: // 底部位置,y坐标向下偏移
return { id: uuid(), x: box.x, y: box.y + offset };
case Position.Left: // 左侧位置,x坐标向左偏移
return { id: uuid(), x: box.x - offset, y: box.y };
case Position.Right: // 右侧位置,x坐标向右偏移
return { id: uuid(), x: box.x + offset, y: box.y };
}
};
/**
* 线
* @param p -
* @param p1 - 线
* @param p2 - 线
* @returns 线true,false
*
* :
* 1. 线线(x坐标相等或y坐标相等)
* 2. 线
*/
export const isInLine = (
p: ControlPoint,
p1: ControlPoint,
p2: ControlPoint
) => {
// 获取x坐标的范围区间[min, max]
const xPoints = p1.x < p2.x ? [p1.x, p2.x] : [p2.x, p1.x];
// 获取y坐标的范围区间[min, max]
const yPoints = p1.y < p2.y ? [p1.y, p2.y] : [p2.y, p1.y];
return (
// 垂直线段:三点x坐标相等,且待判断点的y坐标在范围内
(p1.x === p.x && p.x === p2.x && p.y >= yPoints[0] && p.y <= yPoints[1]) ||
// 水平线段:三点y坐标相等,且待判断点的x坐标在范围内
(p1.y === p.y && p.y === p2.y && p.x >= xPoints[0] && p.x <= xPoints[1])
);
};
/**
* 线(线)
* @param p -
* @param p1 - 线1
* @param p2 - 线2
* @returns 线true,false
*
* :
* 线线(x坐标相等或y坐标相等)
*/
export const isOnLine = (
p: ControlPoint,
p1: ControlPoint,
p2: ControlPoint
) => {
return (p1.x === p.x && p.x === p2.x) || (p1.y === p.y && p.y === p2.y);
};
export interface OptimizePointsParams {
edgePoints: ControlPoint[];
source: HandlePosition;
target: HandlePosition;
sourceOffset: ControlPoint;
targetOffset: ControlPoint;
}
/**
*
*
* :
* 1.
* 2.
* 3.
*
* @param p
* @returns ,
*/
export const optimizeInputPoints = (p: OptimizePointsParams) => {
// 合并坐标相近的点,将所有点放入一个数组进行处理
let edgePoints = mergeClosePoints([
p.source,
p.sourceOffset,
...p.edgePoints,
p.targetOffset,
p.target,
]);
// 从合并后的点中提取起点和终点
const source = edgePoints.shift()!;
const target = edgePoints.pop()!;
const sourceOffset = edgePoints[0];
const targetOffset = edgePoints[edgePoints.length - 1];
// 根据起点和终点的位置类型修正其坐标
// 如果是水平方向,则保持x坐标不变;否则保持y坐标不变
if (isHorizontalFromPosition(p.source.position)) {
source.x = p.source.x;
} else {
source.y = p.source.y;
}
if (isHorizontalFromPosition(p.target.position)) {
target.x = p.target.x;
} else {
target.y = p.target.y;
}
// 移除重复的坐标点,并为每个点分配唯一ID
edgePoints = removeRepeatPoints(edgePoints).map((p, idx) => ({
...p,
id: `${idx + 1}`,
}));
return { source, target, sourceOffset, targetOffset, edgePoints };
};
/**
*
*
* :
* 1. 线
* 2. 线
*
* :
* -
* - 线
* - 线
*
* @param points
* @returns
*/
export function reducePoints(points: ControlPoint[]): ControlPoint[] {
const optimizedPoints = [points[0]];
// 遍历除首尾点外的所有点
for (let i = 1; i < points.length - 1; i++) {
// 判断当前点是否在前后两点形成的直线上
const inSegment = isInLine(points[i], points[i - 1], points[i + 1]);
// 如果不在直线上,则保留该点
if (!inSegment) {
optimizedPoints.push(points[i]);
}
}
optimizedPoints.push(points[points.length - 1]);
return optimizedPoints;
}
/**
*
* ,
*/
/**
* ,
* @param points
* @param threshold ,4
* @returns
*
* :
* 1. x和y轴上的所有坐标值
* 2. ,
* 3. 使,
*/
export function mergeClosePoints(
points: ControlPoint[],
threshold = 4
): ControlPoint[] {
// 存储已处理的离散坐标值
const positions = { x: [] as number[], y: [] as number[] };
/**
*
* @param axis ('x'|'y')
* @param v
* @returns 使
*/
const findPosition = (axis: "x" | "y", v: number) => {
// 向下取整,确保坐标为整数
v = Math.floor(v);
const ps = positions[axis];
// 在阈值范围内查找已存在的相近值
let p = ps.find((e) => Math.abs(v - e) < threshold);
// 如果没找到相近值,则添加新值
if (p == null) {
p = v;
positions[axis].push(v);
}
return p;
};
// 处理每个控制点的坐标
const finalPoints = points.map((point) => {
return {
...point,
x: findPosition("x", point.x),
y: findPosition("y", point.y),
};
});
return finalPoints;
}
/**
*
* @param p1 1
* @param p2 2
* @returns
*/
export function isEqualPoint(p1: ControlPoint, p2: ControlPoint) {
return p1.x === p2.x && p1.y === p2.y;
}
/**
* ,
* @param points
* @returns
*
* :
* 1. 使Set存储已处理的坐标字符串(:"x-y")
* 2. ()
* 3. ,
*/
export function removeRepeatPoints(points: ControlPoint[]): ControlPoint[] {
// 先添加终点坐标,确保终点被保留
const lastP = points[points.length - 1];
const uniquePoints = new Set([`${lastP.x}-${lastP.y}`]);
const finalPoints: ControlPoint[] = [];
points.forEach((p, idx) => {
// 处理终点
if (idx === points.length - 1) {
return finalPoints.push(p);
}
// 使用坐标字符串作为唯一标识
const key = `${p.x}-${p.y}`;
if (!uniquePoints.has(key)) {
uniquePoints.add(key);
finalPoints.push(p);
}
});
return finalPoints;
}
/**
* 线
* @param p0 线
* @param p1 线
* @param p2 线
* @param p3 线
* @returns 线true,false
*
* :
* 1. 使线
* 2. 使s和t
* 3. [0,1]
*/
const isSegmentsIntersected = (
p0: ControlPoint,
p1: ControlPoint,
p2: ControlPoint,
p3: ControlPoint
): boolean => {
// 计算两条线段的方向向量
const s1x = p1.x - p0.x;
const s1y = p1.y - p0.y;
const s2x = p3.x - p2.x;
const s2y = p3.y - p2.y;
// 使用向量叉积判断两线段是否平行
if (s1x * s2y - s1y * s2x === 0) {
// 平行线段必不相交
return false;
}
// 求解参数方程,获取交点参数s和t
const denominator = -s2x * s1y + s1x * s2y;
const s = (s1y * (p2.x - p0.x) - s1x * (p2.y - p0.y)) / denominator;
const t = (s2x * (p0.y - p2.y) - s2y * (p0.x - p2.x)) / denominator;
// 当且仅当s和t都在[0,1]区间内时,两线段相交
return s >= 0 && s <= 1 && t >= 0 && t <= 1;
};
/**
* 线
* @param p1 线
* @param p2 线
* @param box
* @returns 线true,false
*
* :
* 1. 首先处理特殊情况:矩形退化为点时必不相交
* 2.
* 3. 线
* 4. ,
*/
export const isSegmentCrossingRect = (
p1: ControlPoint,
p2: ControlPoint,
box: NodeRect
): boolean => {
// 处理特殊情况:矩形退化为点
if (box.width === 0 && box.height === 0) {
return false;
}
// 获取矩形的四个顶点
const [topLeft, topRight, bottomRight, bottomLeft] = getRectVertices(box);
// 判断线段是否与矩形的任意一条边相交
return (
isSegmentsIntersected(p1, p2, topLeft, topRight) || // 上边
isSegmentsIntersected(p1, p2, topRight, bottomRight) || // 右边
isSegmentsIntersected(p1, p2, bottomRight, bottomLeft) || // 下边
isSegmentsIntersected(p1, p2, bottomLeft, topLeft) // 左边
);
};

View File

@ -1,200 +0,0 @@
import { deepClone, lastOf } from "@/utils/base";
import { Position, getBezierPath } from "reactflow";
import { getBasePath } from ".";
import {
kBaseMarkerColor,
kBaseMarkerColors,
kNoMarkerColor,
kYesMarkerColor,
} from "../../components/Edges/Marker";
import { isEqual } from "../../utils/diff";
import { EdgeLayout, ReactFlowEdgeWithData } from "../../data/types";
import { kReactFlow } from "../../states/reactflow";
import { getPathWithRoundCorners } from "./edge";
interface EdgeStyle {
color: string;
edgeType: "solid" | "dashed";
pathType: "base" | "bezier";
}
/**
* Get the style of the connection line
*
* 1. When there are more than 3 edges connecting to both ends of the Node, use multiple colors to distinguish the edges.
* 2. When the connection line goes backward or connects to a hub Node, use dashed lines to distinguish the edges.
* 3. When the connection line goes from a hub to a Node, use bezier path.
*/
export const getEdgeStyles = (props: {
id: string;
isBackward: boolean;
}): EdgeStyle => {
const { id, isBackward } = props;
const idx = parseInt(lastOf(id.split("#")) ?? "0", 10);
if (isBackward) {
// Use dashed lines to distinguish the edges when the connection line goes backward or connects to a hub Node
return { color: kNoMarkerColor, edgeType: "dashed", pathType: "base" };
}
const edge: ReactFlowEdgeWithData = kReactFlow.instance!.getEdge(id)!;
if (edge.data!.targetPort.edges > 2) {
// Use dashed bezier path when the connection line connects to a hub Node
return {
color: kYesMarkerColor,
edgeType: "dashed",
pathType: "bezier",
};
}
if (edge.data!.sourcePort.edges > 2) {
// Use multiple colors to distinguish the edges when there are more than 3 edges connecting to both ends of the Node
return {
color: kBaseMarkerColors[idx % kBaseMarkerColors.length],
edgeType: "solid",
pathType: "base",
};
}
return { color: kBaseMarkerColor, edgeType: "solid", pathType: "base" };
};
interface ILayoutEdge {
id: string;
layout?: EdgeLayout;
offset: number;
borderRadius: number;
pathType: EdgeStyle["pathType"];
source: string;
target: string;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
}
export function layoutEdge({
id,
layout,
offset,
borderRadius,
pathType,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: ILayoutEdge): EdgeLayout {
const relayoutDeps = [sourceX, sourceY, targetX, targetY];
const needRelayout = !isEqual(relayoutDeps, layout?.deps?.relayoutDeps);
const reBuildPathDeps = layout?.points;
const needReBuildPath = !isEqual(
reBuildPathDeps,
layout?.deps?.reBuildPathDeps
);
let newLayout = layout;
if (needRelayout) {
newLayout = _layoutEdge({
id,
offset,
borderRadius,
pathType,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
} else if (needReBuildPath) {
newLayout = _layoutEdge({
layout,
id,
offset,
borderRadius,
pathType,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
}
newLayout!.deps = deepClone({ relayoutDeps, reBuildPathDeps });
return newLayout!;
}
function _layoutEdge({
id,
layout,
offset,
borderRadius,
pathType,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: ILayoutEdge): EdgeLayout {
const _pathType: EdgeStyle["pathType"] = pathType;
if (_pathType === "bezier") {
const [path, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
const points = [
{
id: "source-" + id,
x: sourceX,
y: sourceY,
},
{
id: "target-" + id,
x: targetX,
y: targetY,
},
];
return {
path,
points,
inputPoints: points,
labelPosition: {
x: labelX,
y: labelY,
},
};
}
if ((layout?.points?.length ?? 0) > 1) {
layout!.path = getPathWithRoundCorners(layout!.points, borderRadius);
return layout!;
}
return getBasePath({
id,
offset,
borderRadius,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
}

View File

@ -1,34 +0,0 @@
import { Node, Edge } from "@xyflow/react";
import { LayoutOptions, LayoutStrategy } from "./types";
import { TreeLayout } from "./TreeLayout";
import { MindMapLayout } from "./MindMapLayout";
import { SingleMapLayout } from "./SingleMapLayout";
// 布局工厂类
class LayoutFactory {
static createLayout(type: 'mindmap' | 'tree' | 'force' | 'single'): LayoutStrategy {
switch (type) {
case 'mindmap':
return new MindMapLayout();
case 'tree':
return new TreeLayout();
case 'single':
return new SingleMapLayout()
case 'force':
// return new ForceLayout(); // 待实现
default:
return new MindMapLayout();
}
}
}
// 导出布局函数
export function getLayout(type: 'mindmap' | 'tree' | 'force' | 'single', options: LayoutOptions) {
const layoutStrategy = LayoutFactory.createLayout(type);
return layoutStrategy.layout(options);
}
// 为了保持向后兼容,保留原有的导出
export function getMindMapLayout(options: LayoutOptions) {
return getLayout("single", options);
}

View File

@ -1,148 +0,0 @@
import { MarkerType, Position, useInternalNode, Node, Edge } from "@xyflow/react";
import { LayoutDirection, LayoutVisibility } from "./node";
/**
*
* @param nodes -
* @param edges -
* @returns
*/
export const getRootNodes = (nodes: Node[], edges: Edge[]): Node[] => {
// 创建一个Set来存储所有有入边的节点ID
const nodesWithIncoming = new Set(
edges.map((edge) => edge.target)
);
// 过滤出没有入边的节点
const rootNodes = nodes.filter(
(node) => !nodesWithIncoming.has(node.id)
);
return rootNodes;
};
/**
*
* @param node
* @param defaultSize ,(150px)(36px)
* @returns ,:
* - hasDimension: 是否已设置实际尺寸
* - width: 节点实际宽度
* - height: 节点实际高度
* - widthWithDefault: 实际宽度或默认宽度
* - heightWithDefault: 实际高度或默认高度
*/
export const getNodeSize = (
node: Node,
defaultSize = { width: 150, height: 36 }
) => {
// 获取节点的实际宽高
const nodeWith = node?.width;
const nodeHeight = node?.height;
// 检查节点是否同时设置了宽度和高度
const hasDimension = [nodeWith, nodeHeight].every((e) => e != null);
// 返回包含完整尺寸信息的对象
// 使用空值合并运算符(??)在实际尺寸未设置时使用默认值
return {
hasDimension,
width: nodeWith,
height: nodeHeight,
widthWithDefault: nodeWith ?? defaultSize.width,
heightWithDefault: nodeHeight ?? defaultSize.height,
};
};
export type IFixPosition = (pros: {
x: number;
y: number;
width: number;
height: number;
}) => {
x: number;
y: number;
};
/**
*
* @description ,
* @param props
* @param props.node
* @param props.position
* @param props.direction ,'horizontal','vertical'
* @param props.visibility ,'visible',
* @param props.fixPosition ,
* @returns
*/
export const getNodeLayouted = (props: {
node: Node;
position: { x: number; y: number };
direction: LayoutDirection;
visibility: LayoutVisibility;
fixPosition?: IFixPosition;
}) => {
// 解构布局参数,设置位置修正函数的默认值
const {
node,
position,
direction,
visibility,
fixPosition = (p) => ({ x: p.x, y: p.y }),
} = props;
// 计算节点的显示状态和布局方向
const hidden = visibility !== "visible";
const isHorizontal = direction === "horizontal";
// 获取节点尺寸信息
const { width, height, widthWithDefault, heightWithDefault } =
getNodeSize(node);
// 根据布局方向设置节点的连接点位置
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// 返回带有完整布局属性的节点对象
return {
...node,
width,
height,
hidden,
position: fixPosition({
...position,
width: widthWithDefault,
height: heightWithDefault,
}),
style: {
...node.style,
opacity: hidden ? 0 : 1,
},
};
};
/**
*
* @description ,
* @param props
* @param props.edge
* @param props.visibility ,'visible',
* @returns
*/
export const getEdgeLayouted = (props: {
edge: Edge;
visibility: LayoutVisibility;
}) => {
const { edge, visibility } = props;
const hidden = visibility !== "visible";
// 返回带有完整布局属性的边对象
return {
...edge,
hidden,
markerEnd: {
type: MarkerType.ArrowClosed, // 设置箭头样式为闭合箭头
},
style: {
...edge.style,
opacity: hidden ? 0 : 1,
},
};
};

View File

@ -1,121 +0,0 @@
import { graphStratify, sugiyama } from "d3-dag";
import { getIncomers, type Node } from "@xyflow/react";
import { getEdgeLayouted, getNodeLayouted, getNodeSize } from "../../metadata";
import { LayoutAlgorithm, LayoutAlgorithmProps } from "..";
type NodeWithPosition = Node & { x: number; y: number };
// Since d3-dag layout algorithm does not support multiple root nodes,
// we attach the sub-workflows to the global rootNode.
const rootNode: NodeWithPosition = {
id: "#root",
x: 0,
y: 0,
position: { x: 0, y: 0 },
data: {} as any,
};
const algorithms = {
"d3-dag": "d3-dag",
"ds-dag(s)": "ds-dag(s)",
};
export type D3DAGLayoutAlgorithms = "d3-dag" | "ds-dag(s)";
export const layoutD3DAG = async (
props: LayoutAlgorithmProps & { algorithm?: D3DAGLayoutAlgorithms }
) => {
const {
nodes,
edges,
direction,
visibility,
spacing,
algorithm = "d3-dag",
} = props;
const isHorizontal = direction === "horizontal";
const initialNodes = [] as NodeWithPosition[];
let maxNodeWidth = 0;
let maxNodeHeight = 0;
for (const node of nodes) {
const { widthWithDefault, heightWithDefault } = getNodeSize(node);
initialNodes.push({
...node,
...node.position,
width: widthWithDefault,
height: heightWithDefault,
});
maxNodeWidth = Math.max(maxNodeWidth, widthWithDefault);
maxNodeHeight = Math.max(maxNodeHeight, heightWithDefault);
}
// Since d3-dag does not support horizontal layout,
// we swap the width and height of nodes and interchange x and y mappings based on the layout direction.
const nodeSize: any = isHorizontal
? [maxNodeHeight + spacing.y, maxNodeWidth + spacing.x]
: [maxNodeWidth + spacing.x, maxNodeHeight + spacing.y];
const getParentIds = (node: Node) => {
if (node.id === rootNode.id) {
return undefined;
}
// Node without input is the root node of sub-workflow, and we should connect it to the rootNode
const incomers = getIncomers(node, nodes, edges);
if (incomers.length < 1) {
return [rootNode.id];
}
return algorithm === "d3-dag"
? [incomers[0]?.id]
: incomers.map((e) => e.id);
};
const stratify = graphStratify();
const dag = stratify(
[rootNode, ...initialNodes].map((node) => {
return {
id: node.id,
parentIds: getParentIds(node),
};
})
);
const layout = sugiyama().nodeSize(nodeSize);
layout(dag);
const layoutNodes = new Map<string, any>();
for (const node of dag.nodes()) {
layoutNodes.set(node.data.id, node);
}
return {
nodes: nodes.map((node) => {
const { x, y } = layoutNodes.get(node.id);
// Interchange x and y mappings based on the layout direction.
const position = isHorizontal ? { x: y, y: x } : { x, y };
return getNodeLayouted({
node,
position,
direction,
visibility,
fixPosition: ({ x, y, width, height }) => {
// This algorithm uses the center coordinate of the node as the reference point,
// which needs adjustment for ReactFlow's topLeft coordinate system.
return {
x: x - width / 2,
y: y - height / 2,
};
},
});
}),
edges: edges.map((edge) => getEdgeLayouted({ edge, visibility })),
};
};
export const kD3DAGAlgorithms: Record<string, LayoutAlgorithm> = Object.keys(
algorithms
).reduce((pre, algorithm) => {
pre[algorithm] = (props: any) => {
return layoutD3DAG({ ...props, algorithm });
};
return pre;
}, {} as any);

View File

@ -1,89 +0,0 @@
// Based on: https://github.com/flanksource/flanksource-ui/blob/75b35591d3bbc7d446fa326d0ca7536790f38d88/src/ui/Graphs/Layouts/algorithms/d3-hierarchy.ts
import { stratify, tree, type HierarchyPointNode } from "d3-hierarchy";
import {getIncomers, Node} from "@xyflow/react"
import { LayoutAlgorithm } from "..";
import { getEdgeLayouted, getNodeLayouted, getNodeSize } from "../../metadata";
type NodeWithPosition = Node & { x: number; y: number };
const layout = tree<NodeWithPosition>().separation(() => 1);
// Since d3-hierarchy layout algorithm does not support multiple root nodes,
// we attach the sub-workflows to the global rootNode.
const rootNode: NodeWithPosition = {
id: "#root",
x: 0,
y: 0,
position: { x: 0, y: 0 },
data: {} as any,
};
export const layoutD3Hierarchy: LayoutAlgorithm = async (props) => {
const { nodes, edges, direction, visibility, spacing } = props;
const isHorizontal = direction === "horizontal";
const initialNodes = [] as NodeWithPosition[];
let maxNodeWidth = 0;
let maxNodeHeight = 0;
for (const node of nodes) {
const { widthWithDefault, heightWithDefault } = getNodeSize(node);
initialNodes.push({
...node,
...node.position,
width: widthWithDefault,
height: heightWithDefault,
});
maxNodeWidth = Math.max(maxNodeWidth, widthWithDefault);
maxNodeHeight = Math.max(maxNodeHeight, heightWithDefault);
}
// Since d3-hierarchy does not support horizontal layout,
// we swap the width and height of nodes and interchange x and y mappings based on the layout direction.
const nodeSize: [number, number] = isHorizontal
? [maxNodeHeight + spacing.y, maxNodeWidth + spacing.x]
: [maxNodeWidth + spacing.x, maxNodeHeight + spacing.y];
layout.nodeSize(nodeSize);
const getParentId = (node: Node) => {
if (node.id === rootNode.id) {
return undefined;
}
// Node without input is the root node of sub-workflow, and we should connect it to the rootNode
const incomers = getIncomers(node, nodes, edges);
return incomers[0]?.id || rootNode.id;
};
const hierarchy = stratify<NodeWithPosition>()
.id((d) => d.id)
.parentId(getParentId)([rootNode, ...initialNodes]);
const root = layout(hierarchy);
const layoutNodes = new Map<string, HierarchyPointNode<NodeWithPosition>>();
for (const node of root) {
layoutNodes.set(node.id!, node);
}
return {
nodes: nodes.map((node) => {
const { x, y } = layoutNodes.get(node.id)!;
// Interchange x and y mappings based on the layout direction.
const position = isHorizontal ? { x: y, y: x } : { x, y };
return getNodeLayouted({
node,
position,
direction,
visibility,
fixPosition: ({ x, y, width, height }) => {
// This algorithm uses the center coordinate of the node as the reference point,
// which needs adjustment for ReactFlow's topLeft coordinate system.
return {
x: x - width / 2,
y: y - height / 2,
};
},
});
}),
edges: edges.map((edge) => getEdgeLayouted({ edge, visibility })),
};
};

View File

@ -1,122 +0,0 @@
import dagre from "@dagrejs/dagre";
import { LayoutAlgorithm } from "..";
import { getIncomers, Node } from "@xyflow/react";
import { getEdgeLayouted, getNodeLayouted, getNodeSize } from "../../metadata";
import { randomInt } from "../../../utils/base";
// 布局配置常量
const LAYOUT_CONFIG = {
VIRTUAL_ROOT_ID: '#root',
VIRTUAL_NODE_SIZE: 1,
RANKER: 'tight-tree',
} as const;
// 创建并配置 dagre 图实例
const createDagreGraph = () => {
const graph = new dagre.graphlib.Graph();
graph.setDefaultEdgeLabel(() => ({}));
return graph;
};
// 获取布局方向配置
const getLayoutConfig = (
direction: 'horizontal' | 'vertical',
spacing: { x: number, y: number },
graph: dagre.graphlib.Graph
) => ({
nodesep: direction === 'horizontal' ? spacing.y : spacing.x,
ranksep: direction === 'horizontal' ? spacing.x : spacing.y,
ranker: LAYOUT_CONFIG.RANKER,
rankdir: direction === 'horizontal' ? 'LR' : 'TB',
});
// 查找根节点
const findRootNodes = (nodes: Node[], edges: any[]): Node[] =>
nodes.filter(node => getIncomers(node, nodes, edges).length < 1);
// 计算节点边界
const calculateBounds = (nodes: Node[], graph: dagre.graphlib.Graph) => {
const bounds = {
minX: Number.POSITIVE_INFINITY,
minY: Number.POSITIVE_INFINITY,
maxX: Number.NEGATIVE_INFINITY,
maxY: Number.NEGATIVE_INFINITY,
};
nodes.forEach(node => {
const pos = graph.node(node.id);
if (pos) {
bounds.minX = Math.min(bounds.minX, pos.x);
bounds.minY = Math.min(bounds.minY, pos.y);
bounds.maxX = Math.max(bounds.maxX, pos.x);
bounds.maxY = Math.max(bounds.maxY, pos.y);
}
});
return bounds;
};
export const layoutDagreTree: LayoutAlgorithm = async ({
nodes,
edges,
direction,
visibility,
spacing
}) => {
const dagreGraph = createDagreGraph();
// 设置图的布局参数
dagreGraph.setGraph(getLayoutConfig(direction, spacing, dagreGraph));
// 添加节点
nodes.forEach((node) => {
const { widthWithDefault, heightWithDefault } = getNodeSize(node);
dagreGraph.setNode(node.id, {
width: widthWithDefault,
height: heightWithDefault,
order: randomInt(0, 10)
});
});
// 添加边
edges.forEach(edge => dagreGraph.setEdge(edge.source, edge.target));
// 处理多个子工作流的情况
const rootNodes = findRootNodes(nodes, edges);
if (rootNodes.length > 1) {
dagreGraph.setNode(LAYOUT_CONFIG.VIRTUAL_ROOT_ID, {
width: LAYOUT_CONFIG.VIRTUAL_NODE_SIZE,
height: LAYOUT_CONFIG.VIRTUAL_NODE_SIZE,
rank: -1 // 确保虚拟根节点排在最前面
});
rootNodes.forEach(node =>
dagreGraph.setEdge(LAYOUT_CONFIG.VIRTUAL_ROOT_ID, node.id)
);
}
// 执行布局
dagre.layout(dagreGraph);
// 移除虚拟根节点
if (rootNodes.length > 1) {
dagreGraph.removeNode(LAYOUT_CONFIG.VIRTUAL_ROOT_ID);
}
// 计算边界并返回布局结果
const bounds = calculateBounds(nodes, dagreGraph);
return {
nodes: nodes.map(node => getNodeLayouted({
node,
position: dagreGraph.node(node.id),
direction,
visibility,
fixPosition: ({ x, y, width, height }) => ({
x: x - width / 2 - bounds.minX,
y: y - height / 2 - bounds.minY,
}),
})),
edges: edges.map(edge => getEdgeLayouted({ edge, visibility })),
};
};

View File

@ -1,128 +0,0 @@
import ELK, { ElkNode } from "elkjs/lib/elk.bundled.js";
import { getIncomers,Node } from "@xyflow/react";
import { LayoutAlgorithm, LayoutAlgorithmProps } from "..";
import { getEdgeLayouted, getNodeLayouted, getNodeSize } from "../../metadata";
const algorithms = {
"elk-layered": "layered",
"elk-mr-tree": "mrtree",
};
const elk = new ELK({ algorithms: Object.values(algorithms) });
export type ELKLayoutAlgorithms = "elk-layered" | "elk-mr-tree";
export const layoutELK = async (
props: LayoutAlgorithmProps & { algorithm?: ELKLayoutAlgorithms }
) => {
const {
nodes,
edges,
direction,
visibility,
spacing,
algorithm = "elk-mr-tree",
} = props;
const isHorizontal = direction === "horizontal";
const subWorkflowRootNodes: Node[] = [];
const layoutNodes = nodes.map((node) => {
const incomers = getIncomers(node, nodes, edges);
if (incomers.length < 1) {
// Node without input is the root node of sub-workflow
subWorkflowRootNodes.push(node);
}
const { widthWithDefault, heightWithDefault } = getNodeSize(node);
const sourcePorts = node.data.sourceHandles.map((id) => ({
id,
properties: {
side: isHorizontal ? "EAST" : "SOUTH",
},
}));
const targetPorts = node.data.targetHandles.map((id) => ({
id,
properties: {
side: isHorizontal ? "WEST" : "NORTH",
},
}));
return {
id: node.id,
width: widthWithDefault,
height: heightWithDefault,
ports: [...targetPorts, ...sourcePorts],
properties: {
"org.eclipse.elk.portConstraints": "FIXED_ORDER",
},
};
});
const layoutEdges = edges.map((edge) => {
return {
id: edge.id,
sources: [edge.sourceHandle || edge.source],
targets: [edge.targetHandle || edge.target],
};
});
// Connect sub-workflows' root nodes to the rootNode
const rootNode: any = { id: "#root", width: 1, height: 1 };
layoutNodes.push(rootNode);
for (const subWorkflowRootNode of subWorkflowRootNodes) {
layoutEdges.push({
id: `${rootNode.id}-${subWorkflowRootNode.id}`,
sources: [rootNode.id],
targets: [subWorkflowRootNode.id],
});
}
const layouted = await elk
.layout({
id: "@root",
children: layoutNodes,
edges: layoutEdges,
layoutOptions: {
// - https://www.eclipse.org/elk/reference/algorithms.html
"elk.algorithm": algorithms[algorithm],
"elk.direction": isHorizontal ? "RIGHT" : "DOWN",
// - https://www.eclipse.org/elk/reference/options.html
"elk.spacing.nodeNode": isHorizontal
? spacing.y.toString()
: spacing.x.toString(),
"elk.layered.spacing.nodeNodeBetweenLayers": isHorizontal
? spacing.x.toString()
: spacing.y.toString(),
},
})
.catch((e) => {
console.log("❌ ELK layout failed", e);
}) as ElkNode
if (!layouted?.children) {
return;
}
const layoutedNodePositions = layouted.children.reduce((pre, v) => {
pre[v.id] = {
x: v.x ?? 0,
y: v.y ?? 0,
};
return pre;
}, {} as Record<string, { x: number; y: number }>);
return {
nodes: nodes.map((node) => {
const position = layoutedNodePositions[node.id];
return getNodeLayouted({ node, position, direction, visibility });
}),
edges: edges.map((edge) => getEdgeLayouted({ edge, visibility })),
};
};
export const kElkAlgorithms: Record<string, LayoutAlgorithm> = Object.keys(
algorithms
).reduce((pre, algorithm) => {
pre[algorithm] = (props: any) => {
return layoutELK({ ...props, algorithm });
};
return pre;
}, {} as any);

View File

@ -1,20 +0,0 @@
import { LayoutAlgorithm } from "..";
import { getEdgeLayouted, getNodeLayouted } from "../../metadata";
/**
* Positions all nodes at the origin (0,0) in the layout.
*/
export const layoutOrigin: LayoutAlgorithm = async (props) => {
const { nodes, edges, direction, visibility } = props;
return {
nodes: nodes.map((node) => {
return getNodeLayouted({
node,
direction,
visibility,
position: { x: 0, y: 0 },
});
}),
edges: edges.map((edge) => getEdgeLayouted({ edge, visibility })),
};
};

View File

@ -1,149 +0,0 @@
/**
*
*
* ReactFlow
* ,
*
*/
import { ReactFlowGraph } from "../../types";
import { removeEmpty } from "../../utils/base";
import { D3DAGLayoutAlgorithms, kD3DAGAlgorithms } from "./algorithms/d3-dag";
import { layoutD3Hierarchy } from "./algorithms/d3-hierarchy";
import { layoutDagreTree } from "./algorithms/dagre-tree";
import { ELKLayoutAlgorithms, kElkAlgorithms } from "./algorithms/elk";
import { layoutOrigin } from "./algorithms/origin";
/**
*
* vertical: 垂直布局
* horizontal: 水平布局
*/
export type LayoutDirection = "vertical" | "horizontal";
/**
*
* visible: 可见
* hidden: 隐藏
*/
export type LayoutVisibility = "visible" | "hidden";
/**
*
* x: 水平间距
* y: 垂直间距
*/
export interface LayoutSpacing {
x: number;
y: number;
}
/**
* ReactFlow
*
*/
export type ReactFlowLayoutConfig = {
algorithm: LayoutAlgorithms; // 使用的布局算法
direction: LayoutDirection; // 布局方向
spacing: LayoutSpacing; // 节点间距
/**
*
* ,
*/
visibility: LayoutVisibility;
/**
*
*/
reverseSourceHandles: boolean;
autoCenterRoot: boolean
};
/**
*
* ReactFlowGraph ()
*/
export type LayoutAlgorithmProps = ReactFlowGraph &
Omit<ReactFlowLayoutConfig, "algorithm">;
/**
*
* ,
*/
export type LayoutAlgorithm = (
props: LayoutAlgorithmProps
) => Promise<ReactFlowGraph | undefined>;
/**
*
*
*/
export const layoutAlgorithms: Record<string, LayoutAlgorithm> = {
origin: layoutOrigin,
"dagre-tree": layoutDagreTree,
"d3-hierarchy": layoutD3Hierarchy,
...kElkAlgorithms,
...kD3DAGAlgorithms,
};
/**
*
*/
export const defaultLayoutConfig: ReactFlowLayoutConfig = {
algorithm: "dagre-tree", // 默认使用 elk-mr-tree 算法
direction: "horizontal", // 默认垂直布局
visibility: "visible", // 默认可见
spacing: { x: 120, y: 120 }, // 默认间距
reverseSourceHandles: false, // 默认不反转源节点手柄
autoCenterRoot: false
};
/**
*
*/
export type LayoutAlgorithms =
| "origin"
| "dagre-tree"
| "d3-hierarchy"
| ELKLayoutAlgorithms
| D3DAGLayoutAlgorithms;
/**
* ReactFlow
*
*/
export type ReactFlowLayout = ReactFlowGraph & Partial<ReactFlowLayoutConfig>;
/**
* ReactFlow
*
* @param options - ,
* @returns
*
* :
* 1.
* 2.
* 3.
* 4. ,退
*/
export const layoutReactFlow = async (
options: ReactFlowLayout
): Promise<ReactFlowGraph> => {
// 合并配置,移除空值
const config = { ...defaultLayoutConfig, ...removeEmpty(options) };
const { nodes = [], edges = [] } = config;
// 获取并执行布局算法
const layout = layoutAlgorithms[config.algorithm];
let result = await layout({ ...config, nodes, edges });
// 布局失败时回退处理
if (!result) {
result = await layoutReactFlow({
...config,
nodes,
edges,
algorithm: "origin",
});
}
return result!;
};

View File

@ -1,23 +0,0 @@
import { Node, Edge } from "@xyflow/react";
// 基础接口和类型定义
export interface LayoutOptions {
nodes: Node[];
edges: Edge[];
levelSeparation?: number;
nodeSeparation?: number;
}
export interface NodeWithLayout extends Node {
children?: NodeWithLayout[];
parent?: NodeWithLayout;
subtreeHeight?: number;
subtreeWidth?: number;
isRight?: boolean;
relativeY?: number
verticalLevel?: number
}
// 布局策略接口
export interface LayoutStrategy {
layout(options: LayoutOptions): { nodes: Node[], edges: Edge[] };
}

View File

@ -1,179 +0,0 @@
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { Handle, Position, NodeProps, Node, useUpdateNodeInternals } from '@xyflow/react';
import useGraphStore from '../store';
import { shallow } from 'zustand/shallow';
import { GraphState } from '../types';
import { cn } from '@web/src/utils/classname';
import { LEVEL_STYLES, NODE_BASE_STYLES, TEXTAREA_BASE_STYLES } from './style';
export type GraphNode = Node<{
label: string;
color?: string;
level?: number;
}, 'graph-node'>;
interface TextMeasurerProps {
element: HTMLTextAreaElement;
minWidth?: number;
maxWidth?: number;
padding?: number;
}
const measureTextWidth = ({
element,
minWidth = 60,
maxWidth = 400,
padding = 16,
}: TextMeasurerProps): number => {
const span = document.createElement('span');
const styles = {
visibility: 'hidden',
position: 'absolute',
whiteSpace: 'pre',
fontSize: window.getComputedStyle(element).fontSize,
} as const;
Object.assign(span.style, styles);
span.textContent = element.value || element.placeholder;
document.body.appendChild(span);
const contentWidth = Math.min(Math.max(span.offsetWidth + padding, minWidth), maxWidth);
document.body.removeChild(span);
return contentWidth;
};
const selector = (store: GraphState) => ({
updateNode: store.updateNode,
});
export const GraphNode = memo(({ id, selected, width, height, data, isConnectable }: NodeProps<GraphNode>) => {
const { updateNode } = useGraphStore(selector, shallow);
const [isEditing, setIsEditing] = useState(false);
const [inputValue, setInputValue] = useState(data.label);
const [isComposing, setIsComposing] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const updateNodeInternals = useUpdateNodeInternals();
// const [nodeWidth, setNodeWidth] = useState(width)
// const [nodeHeight, setNodeHeight] = useState(height)
const updateTextareaSize = useCallback((element: HTMLTextAreaElement) => {
const contentWidth = measureTextWidth({ element });
element.style.whiteSpace = contentWidth >= 400 ? 'pre-wrap' : 'pre';
element.style.width = `${contentWidth}px`;
element.style.height = 'auto';
element.style.height = `${element.scrollHeight}px`;
}, []);
const handleChange = useCallback((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = evt.target.value;
setInputValue(newValue);
updateNode(id, { label: newValue });
updateTextareaSize(evt.target);
}, [updateNode, id, updateTextareaSize]);
useEffect(() => {
if (textareaRef.current) {
updateTextareaSize(textareaRef.current);
}
}, [isEditing, inputValue, updateTextareaSize]);
const handleKeyDown = useCallback((evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isAlphanumeric = /^[a-zA-Z0-9]$/.test(evt.key);
const isSpaceKey = evt.key === ' ';
if (!isEditing && (isAlphanumeric || isSpaceKey)) {
evt.preventDefault();
evt.stopPropagation();
const newValue = isAlphanumeric ? evt.key : data.label;
setIsEditing(true);
setInputValue(newValue);
updateNode(id, { label: newValue });
return;
}
if (isEditing && evt.key === 'Enter' && !evt.shiftKey && !isComposing) {
evt.preventDefault();
setIsEditing(false);
}
}, [isEditing, isComposing, data.label, id, updateNode]);
const handleDoubleClick = useCallback(() => {
setIsEditing(true);
}, []);
const handleBlur = useCallback(() => {
setIsEditing(false);
}, []);
useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
updateNodeInternals(id);
}
});
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<div
ref={containerRef}
onDoubleClick={handleDoubleClick}
className={cn(
NODE_BASE_STYLES,
LEVEL_STYLES[data.level ?? 2].container,
selected && 'ring-2 ring-blue-400',
isEditing && 'ring-2 ring-blue-500'
)}
data-testid="graph-node"
>
<textarea
ref={textareaRef}
value={inputValue}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
className={cn(
TEXTAREA_BASE_STYLES,
LEVEL_STYLES[data.level ?? 2].fontSize,
isEditing ? 'nodrag' : 'cursor-default'
)}
placeholder={isEditing ? "输入节点内容..." : "双击编辑"}
rows={1}
readOnly={!isEditing}
aria-label="节点内容"
/>
<Handle
type="source"
position={Position.Left}
isConnectable={isConnectable}
id="source"
style={{ left: 0 }}
className="w-3 h-3 bg-blue-400 border-2 border-white rounded-full"
/>
<Handle
type="target"
position={Position.Right}
isConnectable={isConnectable}
id="target"
style={{ right: 0 }}
className="w-3 h-3 bg-blue-400 border-2 border-white rounded-full"
/>
</div>
);
});
GraphNode.displayName = 'GraphNode';

View File

@ -1,49 +0,0 @@
export const LEVEL_STYLES = {
0: {
container: `
bg-gradient-to-br from-blue-500 to-blue-600
text-white px-8 py-4
`,
fontSize: 'text-xl font-semibold'
},
1: {
container: `
bg-white
border-2 border-blue-400
text-gray-700 px-4 py-2
hover:border-blue-500
`,
fontSize: 'text-lg'
},
2: {
container: `
bg-gray-50
border border-gray-200
text-gray-600 px-2 py-1
hover:border-blue-300
hover:bg-gray-100
`,
fontSize: 'text-base'
}
} as const;
export const NODE_BASE_STYLES = `
flex items-center justify-center
rounded-xl
min-w-[60px]
w-fit
relative
`;
export const TEXTAREA_BASE_STYLES = `
bg-transparent
text-center
break-words
whitespace-pre-wrap
resize-none
overflow-hidden
outline-none
min-w-0
w-auto
flex-shrink
`;

View File

@ -1,142 +0,0 @@
import { addEdge, applyNodeChanges, applyEdgeChanges, Node, Edge, Connection, NodeChange, EdgeChange } from '@xyflow/react';
import { createWithEqualityFn } from 'zustand/traditional';
import { nanoid } from 'nanoid';
import debounce from 'lodash/debounce';
import { GraphState } from './types';
import { initialEdges, initialNodes } from './data';
const MAX_HISTORY_LENGTH = 100;
const HISTORY_DEBOUNCE_MS = 100;
const useGraphStore = createWithEqualityFn<GraphState>((set, get) => {
return {
past: [],
future: [],
present: {
nodes: initialNodes,
edges: initialEdges,
},
record: (callback: () => void) => {
const currentState = get().present;
console.group('Recording new state');
console.log('Current state:', currentState);
console.log('Past states count:', get().past.length);
console.log('Future states count:', get().future.length);
set(state => {
const newPast = [...state.past.slice(-MAX_HISTORY_LENGTH), currentState];
console.log('New past states count:', newPast.length);
console.groupEnd();
return {
past: newPast,
future: [],
};
});
callback();
},
undo: () => {
const { past, present } = get();
console.group('Undo operation');
console.log('Current state:', present);
console.log('Past states count:', past.length);
if (past.length === 0) {
console.warn('Cannot undo - no past states available');
console.groupEnd();
return;
}
const previous = past[past.length - 1];
const newPast = past.slice(0, past.length - 1);
console.log('Reverting to previous state:', previous);
console.log('New past states count:', newPast.length);
console.log('New future states count:', get().future.length + 1);
console.groupEnd();
set({
past: newPast,
present: previous,
future: [present, ...get().future],
});
},
redo: () => {
const { future, present } = get();
console.group('Redo operation');
console.log('Current state:', present);
console.log('Future states count:', future.length);
if (future.length === 0) {
console.warn('Cannot redo - no future states available');
console.groupEnd();
return;
}
const next = future[0];
const newFuture = future.slice(1);
console.log('Moving to next state:', next);
console.log('New past states count:', get().past.length + 1);
console.log('New future states count:', newFuture.length);
console.groupEnd();
set({
past: [...get().past, present],
present: next,
future: newFuture,
});
},
setNodes: (nodes: Node[]) => {
set(state => ({
present: {
nodes: nodes,
edges: state.present.edges
}
}));
},
setEdges: (edges: Edge[]) => {
set(state => ({
present: {
nodes: state.present.nodes,
edges: edges
}
}));
},
onNodesChange: (changes: NodeChange[]) => {
set(state => ({
present: {
nodes: applyNodeChanges(changes, state.present.nodes),
edges: state.present.edges
}
}))
},
onEdgesChange: (changes: EdgeChange[]) => {
set(state => ({
present: {
nodes: state.present.nodes,
edges: applyEdgeChanges(changes, state.present.edges)
}
}))
},
canUndo: () => get().past.length > 0,
canRedo: () => get().future.length > 0,
updateNode: (nodeId: string, data: any) => {
const newNodes = get().present.nodes.map(node =>
node.id === nodeId ? { ...node, data: { ...node.data, ...data } } : node
);
set({
present: {
nodes: newNodes,
edges: get().present.edges
}
});
},
};
});
export default useGraphStore;

View File

@ -1,94 +0,0 @@
import { Edge, NodeProps, Node, OnConnect, OnEdgesChange, OnNodesChange, Connection, NodeChange, EdgeChange, OnSelectionChangeParams, XYPosition } from "@xyflow/react";
import { GraphEdge } from "./edges/GraphEdge";
import { GraphNode } from "./nodes/GraphNode";
import { ControlPoint } from "./layout/edge/point";
import { ReactFlowLayout, ReactFlowLayoutConfig } from "./layout/node";
// 添加新的类型定义
export type HistoryState = {
nodes: Node[];
edges: Edge[];
type: string; // 记录操作类型
timestamp: number;
};
export type GraphState = {
past: Array<{ nodes: Node[], edges: Edge[] }>;
present: {
nodes: Node[];
edges: Edge[];
};
future: Array<{ nodes: Node[], edges: Edge[] }>;
canUndo: () => boolean;
canRedo: () => boolean;
onNodesChange: (changes: NodeChange[]) => void;
onEdgesChange: (changes: EdgeChange[]) => void;
updateNode: (id: string, data: any) => void;
undo: () => void;
redo: () => void;
setNodes: (nodes: Node[]) => void;
setEdges: (edges: Edge[]) => void;
record: (callback: () => void) => void
};
export const nodeTypes = {
'graph-node': GraphNode
}
export const edgeTypes = {
'graph-edge': GraphEdge
}
export interface ReactFlowGraph {
nodes: Node[]
edges: Edge[]
}
export interface ReactFlowEdgePort {
/**
* Total number of edges in this direction (source or target).
*/
edges: number;
/**
* Number of ports
*/
portCount: number;
/**
* Port's index.
*/
portIndex: number;
/**
* Total number of Edges under the current port.
*/
edgeCount: number;
/**
* Index of the Edge under the current port.
*/
edgeIndex: number;
}
export interface EdgeLayout {
/**
* SVG path for edge rendering
*/
path: string;
/**
* Control points on the edge.
*/
points: ControlPoint[];
labelPosition: XYPosition;
/**
* Current layout dependent variables (re-layout when changed).
*/
deps?: any;
/**
* Potential control points on the edge, for debugging purposes only.
*/
inputPoints: ControlPoint[];
}
export interface ReactFlowEdgeData {
/**
* Data related to the current edge's layout, such as control points.
*/
layout?: EdgeLayout;
sourcePort: ReactFlowEdgePort;
targetPort: ReactFlowEdgePort;
}

View File

@ -1,144 +0,0 @@
import { useCallback, useMemo } from "react";
import { nanoid } from 'nanoid';
import { shallow } from 'zustand/shallow';
import { throttle } from 'lodash';
import { Edge, Node, useReactFlow } from "@xyflow/react";
import { GraphState } from "./types";
import useGraphStore from "./store";
// Store selector
const selector = (store: GraphState) => ({
nodes: store.present.nodes,
edges: store.present.edges,
setNodes: store.setNodes,
setEdges: store.setEdges,
record: store.record
});
// Helper functions
const createNode = (label: string): Node => ({
id: nanoid(6),
type: 'graph-node',
data: { label },
position: { x: 0, y: 0 },
});
const createEdge = (source: string, target: string): Edge => ({
id: nanoid(6),
source,
target,
type: 'graph-edge',
});
export function useGraphOperation() {
const store = useGraphStore(selector, shallow);
const { addEdges, addNodes } = useReactFlow();
const selectedNodes = useMemo(() =>
store.nodes.filter(node => node.selected),
[store.nodes]
);
// Find parent node ID for a given node
const findParentId = useCallback((nodeId: string) => {
const parentEdge = store.edges.find(edge => edge.target === nodeId);
return parentEdge?.source;
}, [store.edges]);
// Update node selection
const updateNodeSelection = useCallback((nodeIds: string[]) => {
return store.nodes.map(node => ({
...node,
selected: nodeIds.includes(node.id)
}));
}, [store.nodes]);
// Create new node and connect it
const createConnectedNode = useCallback((parentId: string, deselectOthers = true) => {
const newNode = createNode(`新节点${store.nodes.length}`);
const newEdge = createEdge(parentId, newNode.id);
store.record(() => {
addNodes({ ...newNode, selected: true });
addEdges(newEdge);
if (deselectOthers) {
store.setNodes(updateNodeSelection([newNode.id]));
}
});
}, [store, addNodes, addEdges, updateNodeSelection]);
// Handle node creation operations
const handleCreateChildNodes = useCallback(() => {
if (selectedNodes.length === 0) return;
throttle(() => {
selectedNodes.forEach(node => {
if (node.id) createConnectedNode(node.id);
});
}, 300)();
}, [selectedNodes, createConnectedNode]);
const handleCreateSiblingNodes = useCallback(() => {
if (selectedNodes.length === 0) return;
throttle(() => {
selectedNodes.forEach(node => {
const parentId = findParentId(node.id) || node.id;
createConnectedNode(parentId);
});
}, 300)();
}, [selectedNodes, findParentId, createConnectedNode]);
const handleDeleteNodes = useCallback(() => {
if (selectedNodes.length === 0) return;
const nodesToDelete = new Set<string>();
// Collect all nodes to delete including children
const collectNodesToDelete = (nodeId: string) => {
nodesToDelete.add(nodeId);
store.edges
.filter(edge => edge.source === nodeId)
.forEach(edge => collectNodesToDelete(edge.target));
};
selectedNodes.forEach(node => collectNodesToDelete(node.id));
store.record(() => {
// Filter out deleted nodes and their edges
const remainingNodes = store.nodes.filter(node => !nodesToDelete.has(node.id));
const remainingEdges = store.edges.filter(edge =>
!nodesToDelete.has(edge.source) && !nodesToDelete.has(edge.target)
);
// Select next node (sibling or parent of first deleted node)
const firstDeletedNode = selectedNodes[0];
const parentId = findParentId(firstDeletedNode.id);
let nextSelectedId: string | undefined;
if (parentId) {
const siblingEdge = store.edges.find(edge =>
edge.source === parentId &&
!nodesToDelete.has(edge.target) &&
edge.target !== firstDeletedNode.id
);
nextSelectedId = siblingEdge?.target || parentId;
}
// Update nodes with new selection and set the remaining nodes
const updatedNodes = remainingNodes.map(node => ({
...node,
selected: node.id === nextSelectedId
}));
store.setNodes(updatedNodes);
store.setEdges(remainingEdges);
});
}, [selectedNodes, store, findParentId]);
return {
handleCreateChildNodes,
handleCreateSiblingNodes,
handleDeleteNodes
};
}

View File

@ -1,44 +0,0 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { shallow } from 'zustand/shallow';
import { useGraphOperation } from './useGraphOperation';
import useGraphStore from './store';
import { GraphState } from './types';
const selector = (store: GraphState) => ({
undo: store.undo,
redo: store.redo
});
export function useKeyboardCtrl() {
const { undo, redo } = useGraphStore(selector, shallow);
const {
handleCreateChildNodes,
handleCreateSiblingNodes,
handleDeleteNodes
} = useGraphOperation();
useHotkeys('tab', (e) => {
e.preventDefault();
handleCreateChildNodes();
}, [handleCreateChildNodes]);
useHotkeys('enter', (e) => {
e.preventDefault();
handleCreateSiblingNodes();
}, [handleCreateSiblingNodes]);
useHotkeys('ctrl+z', (e) => {
e.preventDefault();
undo();
}, [undo]);
useHotkeys('ctrl+y', (e) => {
e.preventDefault();
redo();
}, [redo]);
useHotkeys('delete', (e) => {
e.preventDefault();
handleDeleteNodes();
}, [handleDeleteNodes]);
}

View File

@ -1,147 +0,0 @@
import { Position, Node, InternalNode } from "@xyflow/react";
/**
*
* 线
*/
interface IntersectionPoint {
x: number; // 交点的x坐标
y: number; // 交点的y坐标
}
/**
*
*
*/
interface EdgeParams {
sx: number; // 源节点连接点x坐标
sy: number; // 源节点连接点y坐标
tx: number; // 目标节点连接点x坐标
ty: number; // 目标节点连接点y坐标
sourcePos: Position; // 源节点连接位置(上下左右)
targetPos: Position; // 目标节点连接位置(上下左右)
}
/**
*
*
* :
* 线,
* 线,
*
* :
* 1.
* 2.
* 3. 使线
* 4.
*
* @param intersectionNode - ,
* @param targetNode - ,
* @returns {IntersectionPoint} {x, y}
*/
function getNodeIntersection(intersectionNode: InternalNode, targetNode: InternalNode): IntersectionPoint {
// 获取起始节点的宽度和高度
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
// 获取两个节点的绝对位置信息
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
const targetPosition = targetNode.internals.positionAbsolute;
// 计算起始节点的半宽和半高,用于后续的坐标计算
const w = intersectionNodeWidth / 2;
const h = intersectionNodeHeight / 2;
// 计算两个节点的中心点坐标
// (x2,y2)为起始节点的中心点
const x2 = intersectionNodePosition.x + w;
const y2 = intersectionNodePosition.y + h;
// (x1,y1)为目标节点的中心点
const x1 = targetPosition.x + targetNode.measured.width / 2;
const y1 = targetPosition.y + targetNode.measured.height / 2;
// 使用数学公式计算交点坐标
// 这里使用的是参数化方程,将节点边界视为矩形来计算交点
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
// 通过标准化确保交点在节点边界上
const a = 1 / (Math.abs(xx1) + Math.abs(yy1));
const xx3 = a * xx1;
const yy3 = a * yy1;
// 计算最终的交点坐标
const x = w * (xx3 + yy3) + x2;
const y = h * (-xx3 + yy3) + y2;
return { x, y };
}
/**
*
*
* :
* ,线(///)
*
* :
* 1.
* 2.
* 3. ,
*
* @param node -
* (x,y)(width,height)
* @param intersectionPoint -
* x,y坐标值
* @returns Position - ,(Top/Right/Bottom/Left)
*/
function getEdgePosition(node: InternalNode, intersectionPoint: IntersectionPoint): Position {
// 合并节点的绝对定位信息,确保获取准确的节点位置
const n = { ...node.internals.positionAbsolute, ...node };
// 对坐标进行取整,避免浮点数计算误差
const nx = Math.round(n.x); // 节点左边界x坐标
const ny = Math.round(n.y); // 节点上边界y坐标
const px = Math.round(intersectionPoint.x); // 交点x坐标
const py = Math.round(intersectionPoint.y); // 交点y坐标
// 判断逻辑:通过比较交点与节点各边界的位置关系确定连接位置
// 添加1px的容差值,增强判断的容错性
if (px <= nx + 1) {
return Position.Left; // 交点在节点左侧
}
if (px >= nx + n.measured.width - 1) {
return Position.Right; // 交点在节点右侧
}
if (py <= ny + 1) {
return Position.Top; // 交点在节点上方
}
if (py >= n.y + n.measured.height - 1) {
return Position.Bottom; // 交点在节点下方
}
// 若都不满足,默认返回顶部位置作为连接点
return Position.Top;
}
/**
*
* @param source -
* @param target -
* @returns
*
* 线
*/
export function getEdgeParams(source: InternalNode, target: InternalNode): EdgeParams {
// 计算源节点和目标节点的交点
const sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source);
// 确定连接点在各自节点上的位置
const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
const targetPos = getEdgePosition(target, targetIntersectionPoint);
// 返回所有必要的参数
return {
sx: sourceIntersectionPoint.x,
sy: sourceIntersectionPoint.y,
tx: targetIntersectionPoint.x,
ty: targetIntersectionPoint.y,
sourcePos,
targetPos,
};
}

View File

@ -1,115 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export const nextTick = async (frames = 1) => {
const _nextTick = async (idx: number) => {
return new Promise((resolve) => {
requestAnimationFrame(() => resolve(idx));
});
};
for (let i = 0; i < frames; i++) {
await _nextTick(i);
}
};
export const firstOf = <T = any>(datas?: T[]) =>
datas ? (datas.length < 1 ? undefined : datas[0]) : undefined;
export const lastOf = <T = any>(datas?: T[]) =>
datas ? (datas.length < 1 ? undefined : datas[datas.length - 1]) : undefined;
export const randomInt = (min: number, max?: number) => {
if (!max) {
max = min;
min = 0;
}
return Math.floor(Math.random() * (max - min + 1) + min);
};
export const pickOne = <T = any>(datas: T[]) =>
datas.length < 1 ? undefined : datas[randomInt(datas.length - 1)];
export const range = (start: number, end?: number) => {
if (!end) {
end = start;
start = 0;
}
return Array.from({ length: end - start }, (_, index) => start + index);
};
/**
* clamp(-1,0,1)=0
*/
export function clamp(num: number, min: number, max: number): number {
return num < max ? (num > min ? num : min) : max;
}
export const toSet = <T = any>(datas: T[], byKey?: (e: T) => any) => {
if (byKey) {
const keys: Record<string, boolean> = {};
const newDatas: T[] = [];
datas.forEach((e) => {
const key = jsonEncode({ key: byKey(e) }) as any;
if (!keys[key]) {
newDatas.push(e);
keys[key] = true;
}
});
return newDatas;
}
return Array.from(new Set(datas));
};
export function jsonEncode(obj: any, prettier = false) {
try {
return prettier ? JSON.stringify(obj, undefined, 4) : JSON.stringify(obj);
} catch (error) {
return undefined;
}
}
export function jsonDecode(json: string | undefined) {
if (json == undefined) return undefined;
try {
return JSON.parse(json!);
} catch (error) {
return undefined;
}
}
export function removeEmpty<T = any>(data: T): T {
if (Array.isArray(data)) {
return data.filter((e) => e != undefined) as any;
}
const res = {} as any;
for (const key in data) {
if (data[key] != undefined) {
res[key] = data[key];
}
}
return res;
}
export const deepClone = <T>(obj: T): T => {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
const copy: any[] = [];
obj.forEach((item, index) => {
copy[index] = deepClone(item);
});
return copy as unknown as T;
}
const copy = {} as T;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
(copy as any)[key] = deepClone((obj as any)[key]);
}
}
return copy;
};

View File

@ -1,105 +0,0 @@
// @ts-nocheck
// Source: https://github.com/AsyncBanana/microdiff
interface Difference {
type: "CREATE" | "REMOVE" | "CHANGE";
path: (string | number)[];
value?: any;
}
interface Options {
cyclesFix: boolean;
}
const t = true;
const richTypes = { Date: t, RegExp: t, String: t, Number: t };
export function isEqual(oldObj: any, newObj: any): boolean {
return (
diff(
{
obj: oldObj,
},
{ obj: newObj }
).length < 1
);
}
export const isNotEqual = (oldObj: any, newObj: any) =>
!isEqual(oldObj, newObj);
function diff(
obj: Record<string, any> | any[],
newObj: Record<string, any> | any[],
options: Partial<Options> = { cyclesFix: true },
_stack: Record<string, any>[] = []
): Difference[] {
const diffs: Difference[] = [];
const isObjArray = Array.isArray(obj);
for (const key in obj) {
const objKey = obj[key];
const path = isObjArray ? Number(key) : key;
if (!(key in newObj)) {
diffs.push({
type: "REMOVE",
path: [path],
});
continue;
}
const newObjKey = newObj[key];
const areObjects =
typeof objKey === "object" && typeof newObjKey === "object";
if (
objKey &&
newObjKey &&
areObjects &&
!richTypes[Object.getPrototypeOf(objKey).constructor.name] &&
(options.cyclesFix ? !_stack.includes(objKey) : true)
) {
const nestedDiffs = diff(
objKey,
newObjKey,
options,
options.cyclesFix ? _stack.concat([objKey]) : []
);
// eslint-disable-next-line prefer-spread
diffs.push.apply(
diffs,
nestedDiffs.map((difference) => {
difference.path.unshift(path);
return difference;
})
);
} else if (
objKey !== newObjKey &&
!(
areObjects &&
(Number.isNaN(objKey)
? String(objKey) === String(newObjKey)
: Number(objKey) === Number(newObjKey))
)
) {
diffs.push({
path: [path],
type: "CHANGE",
value: newObjKey,
});
}
}
const isNewObjArray = Array.isArray(newObj);
for (const key in newObj) {
if (!(key in obj)) {
diffs.push({
type: "CREATE",
path: [isNewObjArray ? Number(key) : key],
value: newObj[key],
});
}
}
return diffs;
}

View File

@ -1,11 +0,0 @@
export function uuid(): string {
const uuid = new Array(36);
for (let i = 0; i < 36; i++) {
uuid[i] = Math.floor(Math.random() * 16);
}
uuid[14] = 4;
uuid[19] = uuid[19] &= ~(1 << 2);
uuid[19] = uuid[19] |= 1 << 3;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = "-";
return uuid.map((x) => x.toString(16)).join("");
}

View File

@ -4,7 +4,6 @@ export const env: {
VERSION: string;
FILE_PORT: string;
SERVER_PORT: string;
WEB_PORT: string;
} = {
APP_NAME: import.meta.env.PROD
? (window as any).env.VITE_APP_APP_NAME
@ -15,9 +14,6 @@ export const env: {
FILE_PORT: import.meta.env.PROD
? (window as any).env.VITE_APP_FILE_PORT
: import.meta.env.VITE_APP_FILE_PORT,
WEB_PORT: import.meta.env.PROD
? (window as any).env.VITE_APP_WEB_PORT
: import.meta.env.VITE_APP_WEB_PORT,
SERVER_PORT: import.meta.env.PROD
? (window as any).env.VITE_APP_SERVER_PORT
: import.meta.env.VITE_APP_SERVER_PORT,

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const r=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:40,height:40,viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M20 2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4l4 4l4-4h4a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2m-8 2.3c1.5 0 2.7 1.2 2.7 2.7S13.5 9.7 12 9.7S9.3 8.5 9.3 7s1.2-2.7 2.7-2.7M18 15H6v-.9c0-2 4-3.1 6-3.1s6 1.1 6 3.1z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const h=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const l=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"M12 23C6.443 21.765 2 16.522 2 11V5l10-4l10 4v6c0 5.524-4.443 10.765-10 12M4 6v5a10.58 10.58 0 0 0 8 10a10.58 10.58 0 0 0 8-10V6l-8-3Z"}),e.createElement("circle",{cx:12,cy:8.5,r:2.5,fill:"currentColor"}),e.createElement("path",{fill:"currentColor",d:"M7 15a5.78 5.78 0 0 0 5 3a5.78 5.78 0 0 0 5-3c-.025-1.896-3.342-3-5-3c-1.667 0-4.975 1.104-5 3"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as l}from"./index-C6LPy0O3.js";const h=t=>l.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},l.createElement("path",{fill:"currentColor",d:"m6.923 13.442l1.848-4.75H5.038l-1.267 1.904H3.02l.827-2.865l-.827-2.885h.752L5.04 6.75h3.73L6.923 2h1l3.335 4.75h3.146q.413 0 .697.284q.284.283.284.697t-.284.687q-.284.274-.697.274h-3.146l-3.335 4.75zM16.096 22l-3.335-4.75H9.617q-.414 0-.698-.284q-.283-.283-.283-.697t.283-.697t.698-.284h3.146l3.334-4.73h1l-1.848 4.73h3.733l1.267-1.884H21l-.827 2.865l.827 2.885h-.752l-1.267-1.904h-3.733L17.096 22z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M4.5 20q-.213 0-.356-.144T4 19.499t.144-.356T4.5 19h15q.213 0 .356.144t.144.357t-.144.356T19.5 20zm4-3.75q-.213 0-.356-.144T8 15.749t.144-.356t.356-.143h7q.213 0 .356.144t.144.357t-.144.356t-.356.143zm-4-3.75q-.213 0-.356-.144T4 11.999t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm4-3.75q-.213 0-.356-.144T8 8.249t.144-.356t.356-.143h7q.213 0 .356.144t.144.357t-.144.356t-.356.143zM4.5 5q-.213 0-.356-.144T4 4.499t.144-.356T4.5 4h15q.213 0 .356.144t.144.357t-.144.356T19.5 5z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M4.5 20q-.213 0-.356-.144T4 19.499t.144-.356T4.5 19h15q.213 0 .356.144t.144.357t-.144.356T19.5 20zm0-3.75q-.213 0-.356-.144T4 15.749t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm0-3.75q-.213 0-.356-.144T4 11.999t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm0-3.75q-.213 0-.356-.144T4 8.249t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zM4.5 5q-.213 0-.356-.144T4 4.499t.144-.356T4.5 4h15q.213 0 .356.144t.144.357t-.144.356T19.5 5z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M4.5 20q-.213 0-.356-.144T4 19.499t.144-.356T4.5 19h15q.213 0 .356.144t.144.357t-.144.356T19.5 20zm0-3.75q-.213 0-.356-.144T4 15.749t.144-.356t.356-.143h9q.213 0 .356.144t.144.357t-.144.356t-.356.143zm0-3.75q-.213 0-.356-.144T4 11.999t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm0-3.75q-.213 0-.356-.144T4 8.249t.144-.356t.356-.143h9q.213 0 .356.144t.144.357t-.144.356t-.356.143zM4.5 5q-.213 0-.356-.144T4 4.499t.144-.356T4.5 4h15q.213 0 .356.144t.144.357t-.144.356T19.5 5z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M4.5 5q-.213 0-.356-.144T4 4.499t.144-.356T4.5 4h15q.213 0 .356.144t.144.357t-.144.356T19.5 5zm6 3.75q-.213 0-.356-.144T10 8.249t.144-.356t.356-.143h9q.213 0 .356.144t.144.357t-.144.356t-.356.143zm-6 3.75q-.213 0-.356-.144T4 11.999t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm6 3.75q-.213 0-.356-.144T10 15.749t.144-.356t.356-.143h9q.213 0 .356.144t.144.357t-.144.356t-.356.143zM4.5 20q-.213 0-.356-.144T4 19.499t.144-.356T4.5 19h15q.213 0 .356.144t.144.357t-.144.356T19.5 20z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as l}from"./index-C6LPy0O3.js";const r=t=>l.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},l.createElement("path",{fill:"currentColor",d:"m23 12l-2.44-2.78l.34-3.68l-3.61-.82l-1.89-3.18L12 3L8.6 1.54L6.71 4.72l-3.61.81l.34 3.68L1 12l2.44 2.78l-.34 3.69l3.61.82l1.89 3.18L12 21l3.4 1.46l1.89-3.18l3.61-.82l-.34-3.68zm-13 5l-4-4l1.41-1.41L10 14.17l6.59-6.59L18 9z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const l=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"m11.565 13.873l-2.677-2.677q-.055-.055-.093-.129q-.037-.073-.037-.157q0-.168.11-.289q.112-.121.294-.121h5.677q.181 0 .292.124t.111.288q0 .042-.13.284l-2.677 2.677q-.093.093-.2.143t-.235.05t-.235-.05t-.2-.143"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const r=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:40,height:40,viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M20 2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4l4 4l4-4h4a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2m-8 2.3c1.5 0 2.7 1.2 2.7 2.7S13.5 9.7 12 9.7S9.3 8.5 9.3 7s1.2-2.7 2.7-2.7M18 15H6v-.9c0-2 4-3.1 6-3.1s6 1.1 6 3.1z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const h=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const l=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"M12 23C6.443 21.765 2 16.522 2 11V5l10-4l10 4v6c0 5.524-4.443 10.765-10 12M4 6v5a10.58 10.58 0 0 0 8 10a10.58 10.58 0 0 0 8-10V6l-8-3Z"}),e.createElement("circle",{cx:12,cy:8.5,r:2.5,fill:"currentColor"}),e.createElement("path",{fill:"currentColor",d:"M7 15a5.78 5.78 0 0 0 5 3a5.78 5.78 0 0 0 5-3c-.025-1.896-3.342-3-5-3c-1.667 0-4.975 1.104-5 3"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as l}from"./index-C6LPy0O3.js";const h=t=>l.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},l.createElement("path",{fill:"currentColor",d:"m6.923 13.442l1.848-4.75H5.038l-1.267 1.904H3.02l.827-2.865l-.827-2.885h.752L5.04 6.75h3.73L6.923 2h1l3.335 4.75h3.146q.413 0 .697.284q.284.283.284.697t-.284.687q-.284.274-.697.274h-3.146l-3.335 4.75zM16.096 22l-3.335-4.75H9.617q-.414 0-.698-.284q-.283-.283-.283-.697t.283-.697t.698-.284h3.146l3.334-4.73h1l-1.848 4.73h3.733l1.267-1.884H21l-.827 2.865l.827 2.885h-.752l-1.267-1.904h-3.733L17.096 22z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M4.5 20q-.213 0-.356-.144T4 19.499t.144-.356T4.5 19h15q.213 0 .356.144t.144.357t-.144.356T19.5 20zm4-3.75q-.213 0-.356-.144T8 15.749t.144-.356t.356-.143h7q.213 0 .356.144t.144.357t-.144.356t-.356.143zm-4-3.75q-.213 0-.356-.144T4 11.999t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm4-3.75q-.213 0-.356-.144T8 8.249t.144-.356t.356-.143h7q.213 0 .356.144t.144.357t-.144.356t-.356.143zM4.5 5q-.213 0-.356-.144T4 4.499t.144-.356T4.5 4h15q.213 0 .356.144t.144.357t-.144.356T19.5 5z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M4.5 20q-.213 0-.356-.144T4 19.499t.144-.356T4.5 19h15q.213 0 .356.144t.144.357t-.144.356T19.5 20zm0-3.75q-.213 0-.356-.144T4 15.749t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm0-3.75q-.213 0-.356-.144T4 11.999t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm0-3.75q-.213 0-.356-.144T4 8.249t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zM4.5 5q-.213 0-.356-.144T4 4.499t.144-.356T4.5 4h15q.213 0 .356.144t.144.357t-.144.356T19.5 5z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M4.5 20q-.213 0-.356-.144T4 19.499t.144-.356T4.5 19h15q.213 0 .356.144t.144.357t-.144.356T19.5 20zm0-3.75q-.213 0-.356-.144T4 15.749t.144-.356t.356-.143h9q.213 0 .356.144t.144.357t-.144.356t-.356.143zm0-3.75q-.213 0-.356-.144T4 11.999t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm0-3.75q-.213 0-.356-.144T4 8.249t.144-.356t.356-.143h9q.213 0 .356.144t.144.357t-.144.356t-.356.143zM4.5 5q-.213 0-.356-.144T4 4.499t.144-.356T4.5 4h15q.213 0 .356.144t.144.357t-.144.356T19.5 5z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M4.5 5q-.213 0-.356-.144T4 4.499t.144-.356T4.5 4h15q.213 0 .356.144t.144.357t-.144.356T19.5 5zm6 3.75q-.213 0-.356-.144T10 8.249t.144-.356t.356-.143h9q.213 0 .356.144t.144.357t-.144.356t-.356.143zm-6 3.75q-.213 0-.356-.144T4 11.999t.144-.356t.356-.143h15q.213 0 .356.144t.144.357t-.144.356t-.356.143zm6 3.75q-.213 0-.356-.144T10 15.749t.144-.356t.356-.143h9q.213 0 .356.144t.144.357t-.144.356t-.356.143zM4.5 20q-.213 0-.356-.144T4 19.499t.144-.356T4.5 19h15q.213 0 .356.144t.144.357t-.144.356T19.5 20z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as l}from"./index-C6LPy0O3.js";const r=t=>l.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},l.createElement("path",{fill:"currentColor",d:"m23 12l-2.44-2.78l.34-3.68l-3.61-.82l-1.89-3.18L12 3L8.6 1.54L6.71 4.72l-3.61.81l.34 3.68L1 12l2.44 2.78l-.34 3.69l3.61.82l1.89 3.18L12 21l3.4 1.46l1.89-3.18l3.61-.82l-.34-3.68zm-13 5l-4-4l1.41-1.41L10 14.17l6.59-6.59L18 9z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const l=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"m11.565 13.873l-2.677-2.677q-.055-.055-.093-.129q-.037-.073-.037-.157q0-.168.11-.289q.112-.121.294-.121h5.677q.181 0 .292.124t.111.288q0 .042-.13.284l-2.677 2.677q-.093.093-.2.143t-.235.05t-.235-.05t-.2-.143"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as l}from"./index-C6LPy0O3.js";const t=a=>l.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:40,height:40,viewBox:"0 0 36 36",...a},l.createElement("path",{fill:"currentColor",d:"m33.53 18.76l-6.93-3.19V6.43a1 1 0 0 0-.6-.9l-7.5-3.45a1 1 0 0 0-.84 0l-7.5 3.45a1 1 0 0 0-.58.91v9.14l-6.9 3.18a1 1 0 0 0-.58.91v9.78a1 1 0 0 0 .58.91l7.5 3.45a1 1 0 0 0 .84 0l7.08-3.26l7.08 3.26a1 1 0 0 0 .84 0l7.5-3.45a1 1 0 0 0 .58-.91v-9.78a1 1 0 0 0-.57-.91M25.61 22l-5.11-2.33l5.11-2.35l5.11 2.35Zm-1-6.44l-6.44 3v-7.69a1 1 0 0 0 .35-.08L24.6 8v7.58ZM18.1 4.08l5.11 2.35l-5.11 2.35L13 6.43Zm-7.5 13.23l5.11 2.35L10.6 22l-5.11-2.33Zm6.5 11.49l-6.5 3v-7.69A1 1 0 0 0 11 24l6.08-2.8Zm15 0l-6.46 3v-7.69A1 1 0 0 0 26 24l6.08-2.8Z",className:"clr-i-solid clr-i-solid-path-1"}),l.createElement("path",{fill:"none",d:"M0 0h36v36H0z"}));export{t as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const r=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M8.916 18.25q-.441 0-.74-.299t-.299-.74V6.79q0-.441.299-.74t.74-.299h3.159q1.433 0 2.529.904T15.7 9.006q0 .967-.508 1.693t-1.257 1.065q.913.255 1.55 1.073t.638 1.97q0 1.61-1.202 2.527q-1.202.916-2.646.916zm.236-1.184h3.062q1.161 0 1.875-.7q.715-.699.715-1.627q0-.93-.714-1.629q-.715-.698-1.894-.698H9.152zm0-5.816h2.864q.997 0 1.69-.617t.692-1.546q0-.947-.704-1.553q-.704-.605-1.667-.605H9.152z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const l=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 16 16",...t},e.createElement("path",{fill:"currentColor",fillRule:"evenodd",d:"M6.705 11.823a.73.73 0 0 1-1.205-.552V4.729a.73.73 0 0 1 1.205-.552L10.214 7.2a1 1 0 0 1 .347.757v.084a1 1 0 0 1-.347.757z",clipRule:"evenodd"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const r=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"M11 13.5v8H3v-8zm-2 2H5v4h4zM12 2l5.5 9h-11zm0 3.86L10.08 9h3.84zM17.5 13c2.5 0 4.5 2 4.5 4.5S20 22 17.5 22S13 20 13 17.5s2-4.5 4.5-4.5m0 2a2.5 2.5 0 0 0-2.5 2.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5a2.5 2.5 0 0 0-2.5-2.5"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const l=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"m9.55 18l-5.7-5.7l1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const o=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 48 48",...t},e.createElement("g",{fill:"none",stroke:"currentColor",strokeLinejoin:"round",strokeWidth:4},e.createElement("path",{d:"M24 44a19.94 19.94 0 0 0 14.142-5.858A19.94 19.94 0 0 0 44 24a19.94 19.94 0 0 0-5.858-14.142A19.94 19.94 0 0 0 24 4A19.94 19.94 0 0 0 9.858 9.858A19.94 19.94 0 0 0 4 24a19.94 19.94 0 0 0 5.858 14.142A19.94 19.94 0 0 0 24 44Z"}),e.createElement("path",{strokeLinecap:"round",d:"m16 24l6 6l12-12"})));export{o as default};

View File

@ -0,0 +1 @@
import{r as a}from"./index-C6LPy0O3.js";const h=t=>a.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:40,height:40,viewBox:"0 0 24 24",...t},a.createElement("path",{fill:"currentColor",d:"M13.75 2.25a.75.75 0 0 1 .75.75v4A.75.75 0 0 1 13 7V5.75H3a.75.75 0 0 1 0-1.5h10V3a.75.75 0 0 1 .75-.75M17.25 5a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1-.75-.75m-6.5 4.25a.75.75 0 0 1 .75.75v1.25H21a.75.75 0 0 1 0 1.5h-9.5V14a.75.75 0 0 1-1.5 0v-4a.75.75 0 0 1 .75-.75M2.25 12a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5H3a.75.75 0 0 1-.75-.75m11.5 4.25a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-1.25H3a.75.75 0 0 1 0-1.5h10V17a.75.75 0 0 1 .75-.75m3.5 2.75a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1-.75-.75"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M5.73 15.885h12.54v-1H5.73zm0-3.385h12.54v-1H5.73zm0-3.384h8.77v-1H5.73zM4.616 19q-.69 0-1.153-.462T3 17.384V6.616q0-.691.463-1.153T4.615 5h14.77q.69 0 1.152.463T21 6.616v10.769q0 .69-.463 1.153T19.385 19zm0-1h14.77q.23 0 .423-.192t.192-.424V6.616q0-.231-.192-.424T19.385 6H4.615q-.23 0-.423.192T4 6.616v10.769q0 .23.192.423t.423.192M4 18V6z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const q=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M9.116 17q-.691 0-1.153-.462T7.5 15.385V4.615q0-.69.463-1.153T9.116 3h7.769q.69 0 1.153.462t.462 1.153v10.77q0 .69-.462 1.152T16.884 17zm0-1h7.769q.23 0 .423-.192t.192-.423V4.615q0-.23-.192-.423T16.884 4H9.116q-.231 0-.424.192t-.192.423v10.77q0 .23.192.423t.423.192m-3 4q-.69 0-1.153-.462T4.5 18.385V6.615h1v11.77q0 .23.192.423t.423.192h8.77v1zM8.5 16V4z"}));export{q as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const l=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 256 256",...t},e.createElement("g",{fill:"currentColor"},e.createElement("path",{d:"M128 129.09V232a8 8 0 0 1-3.84-1l-88-48.16a8 8 0 0 1-4.16-7V80.2a8 8 0 0 1 .7-3.27Z",opacity:.2}),e.createElement("path",{d:"m223.68 66.15l-88-48.15a15.88 15.88 0 0 0-15.36 0l-88 48.17a16 16 0 0 0-8.32 14v95.64a16 16 0 0 0 8.32 14l88 48.17a15.88 15.88 0 0 0 15.36 0l88-48.17a16 16 0 0 0 8.32-14V80.18a16 16 0 0 0-8.32-14.03M128 32l80.34 44L128 120L47.66 76ZM40 90l80 43.78v85.79l-80-43.75Zm96 129.57v-85.75L216 90v85.78Z"})));export{l as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const m=h=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 2048 2048",...h},t.createElement("path",{fill:"currentColor",d:"M1792 993q60 41 107 93t81 114t50 131t18 141q0 119-45 224t-124 183t-183 123t-224 46q-91 0-176-27t-156-78t-126-122t-85-157H128V128h256V0h128v128h896V0h128v128h256zM256 256v256h1408V256h-128v128h-128V256H512v128H384V256zm643 1280q-3-31-3-64q0-86 24-167t73-153h-97v-128h128v86q41-51 91-90t108-67t121-42t128-15q100 0 192 33V640H256v896zm573 384q93 0 174-35t142-96t96-142t36-175q0-93-35-174t-96-142t-142-96t-175-36q-93 0-174 35t-142 96t-96 142t-36 175q0 93 35 174t96 142t142 96t175 36m64-512h192v128h-320v-384h128zM384 1024h128v128H384zm256 0h128v128H640zm0-256h128v128H640zm-256 512h128v128H384zm256 0h128v128H640zm384-384H896V768h128zm256 0h-128V768h128zm256 0h-128V768h128z"}));export{m as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const r=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:40,height:40,viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zM17 6H7v13h10zM9 17h2V8H9zm4 0h2V8h-2zM7 6v13z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const o=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"M3 21v-4.25L17.625 2.175L21.8 6.45L7.25 21zM17.6 7.8L19 6.4L17.6 5l-1.4 1.4z"}));export{o as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const h=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"M12 4c-4.42 0-8 3.58-8 8s3.58 8 8 8s8-3.58 8-8s-3.58-8-8-8m1 13h-2v-2h2zm0-4h-2V7h2z",opacity:.3}),e.createElement("path",{fill:"currentColor",d:"M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8m-1-5h2v2h-2zm0-8h2v6h-2z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const r=T=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...T},t.createElement("path",{fill:"currentColor",d:"M12 17q.425 0 .713-.288T13 16t-.288-.712T12 15t-.712.288T11 16t.288.713T12 17m-1-4h2V7h-2zm1 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const l=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 16 16",...t},e.createElement("path",{fill:"currentColor",d:"M12 10V8H7V6h5V4l3 3zm-1-1v4H6v3l-6-3V0h11v5h-1V1H2l4 2v9h4V9z"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const r=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 512 512",...t},e.createElement("path",{fill:"currentColor",d:"M472 168H40a24 24 0 0 1 0-48h432a24 24 0 0 1 0 48m-80 112H120a24 24 0 0 1 0-48h272a24 24 0 0 1 0 48m-96 112h-80a24 24 0 0 1 0-48h80a24 24 0 0 1 0 48"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const a=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 28 28",...t},e.createElement("path",{fill:"currentColor",d:"M13 20.5c0 2.098.862 3.995 2.25 5.357q-1.077.142-2.25.143c-5.79 0-10-2.567-10-6.285V19a3 3 0 0 1 3-3h8.5a7.47 7.47 0 0 0-1.5 4.5M13 2a6 6 0 1 1 0 12a6 6 0 0 1 0-12m14 18.5a6.5 6.5 0 1 1-13 0a6.5 6.5 0 0 1 13 0m-5.786-3.96a.742.742 0 0 0-1.428 0l-.716 2.298h-2.318c-.727 0-1.03.97-.441 1.416l1.875 1.42l-.716 2.298c-.225.721.567 1.32 1.155.875l1.875-1.42l1.875 1.42c.588.446 1.38-.154 1.155-.875l-.716-2.298l1.875-1.42c.588-.445.286-1.416-.441-1.416H21.93z"}));export{a as default};

View File

@ -0,0 +1 @@
import{r as h}from"./index-C6LPy0O3.js";const v=e=>h.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},h.createElement("path",{fill:"currentColor",d:"M11.5 16V9h-3V8h7v1h-3v7zm-9.115 5.616v-4.232H4V6.616H2.385V2.385h4.23V4h10.77V2.385h4.23v4.23H20v10.77h1.616v4.23h-4.232V20H6.616v1.616zM6.615 19h10.77v-1.616H19V6.616h-1.616V5H6.616v1.616H5v10.769h1.616z"}));export{v as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const t=r=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...r},e.createElement("path",{fill:"currentColor",d:"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8"}),e.createElement("circle",{cx:8,cy:14,r:2,fill:"currentColor"}),e.createElement("circle",{cx:12,cy:8,r:2,fill:"currentColor"}),e.createElement("circle",{cx:16,cy:14,r:2,fill:"currentColor"}));export{t as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const o=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("g",{fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:1.5},e.createElement("path",{d:"M2.5 12.89H7l3-5l4 9l3-5h4.43"}),e.createElement("path",{d:"M12 21.5a9.5 9.5 0 1 0 0-19a9.5 9.5 0 0 0 0 19"})));export{o as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const o=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M12 21q-3.45 0-6.012-2.287T3.05 13H5.1q.35 2.6 2.313 4.3T12 19q2.925 0 4.963-2.037T19 12t-2.037-4.962T12 5q-1.725 0-3.225.8T6.25 8H9v2H3V4h2v2.35q1.275-1.6 3.113-2.475T12 3q1.875 0 3.513.713t2.85 1.924t1.925 2.85T21 12t-.712 3.513t-1.925 2.85t-2.85 1.925T12 21m2.8-4.8L11 12.4V7h2v4.6l3.2 3.2z"}));export{o as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const r=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:40,height:40,viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M12 3L2 12h3v8h6v-6h2v6h6v-8h3zm5 15h-2v-6H9v6H7v-7.81l5-4.5l5 4.5z"}),t.createElement("path",{fill:"currentColor",d:"M7 10.19V18h2v-6h6v6h2v-7.81l-5-4.5z",opacity:.3}));export{r as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const o=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M5.5 12.5q-.213 0-.356-.144T5 11.999t.144-.356t.356-.143h13q.213 0 .356.144t.144.357t-.144.356t-.356.143z"}));export{o as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const r=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 50 50",...t},e.createElement("path",{fill:"currentColor",d:"M39 38H11c-1.7 0-3-1.3-3-3V15c0-1.7 1.3-3 3-3h28c1.7 0 3 1.3 3 3v20c0 1.7-1.3 3-3 3M11 14c-.6 0-1 .4-1 1v20c0 .6.4 1 1 1h28c.6 0 1-.4 1-1V15c0-.6-.4-1-1-1z"}),e.createElement("path",{fill:"currentColor",d:"M30 24c-2.2 0-4-1.8-4-4s1.8-4 4-4s4 1.8 4 4s-1.8 4-4 4m0-6c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m5.3 19.7L19 22.4L9.7 31l-1.4-1.4l10.7-10l17.7 16.7z"}),e.createElement("path",{fill:"currentColor",d:"M40.4 32.7L35 28.3L30.5 32l-1.3-1.6l5.8-4.7l6.6 5.4z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as l}from"./index-C6LPy0O3.js";const a=e=>l.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},l.createElement("g",{fill:"none",fillRule:"evenodd"},l.createElement("path",{d:"m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"}),l.createElement("path",{fill:"currentColor",d:"M5.83 5.106A2 2 0 0 1 7.617 4h8.764a2 2 0 0 1 1.789 1.106l3.512 7.025a3 3 0 0 1 .318 1.34V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.528a3 3 0 0 1 .317-1.341zM16.381 6H7.618L4.12 13H7.5A1.5 1.5 0 0 1 9 14.5v1a.5.5 0 0 0 .5.5h5a.5.5 0 0 0 .5-.5v-1a1.5 1.5 0 0 1 1.5-1.5h3.38z"})));export{a as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const l=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M6.346 18.25q-.234 0-.396-.162t-.161-.397t.161-.396t.396-.16h3.077l3.48-10.27H9.828q-.234 0-.396-.162t-.162-.397t.162-.395t.396-.161h7.192q.235 0 .396.162t.162.397t-.162.396q-.161.16-.396.16h-2.961l-3.481 10.27h2.962q.234 0 .395.162t.162.397t-.162.396t-.395.16z"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const h=q=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...q},t.createElement("path",{fill:"currentColor",d:"M17.077 16.077h-2.448q-.194 0-.335-.144q-.14-.143-.14-.356q0-.194.144-.347q.143-.153.356-.153h2.423v-2.423q0-.213.144-.356q.144-.144.357-.144t.356.144t.143.356v2.423H20.5q.213 0 .356.144q.144.144.144.357t-.144.356t-.356.143h-2.423V18.5q0 .213-.144.356t-.357.144t-.356-.144t-.143-.356zm-6.961 0H7.077q-1.692 0-2.884-1.192T3 12t1.193-2.885t2.884-1.193h3.039q.194 0 .347.153t.153.357t-.153.347t-.347.143H7.075q-1.267 0-2.171.904T4 12t.904 2.173t2.17.904h3.042q.194 0 .347.153t.153.356t-.153.348t-.347.143M9 12.5q-.213 0-.356-.144t-.144-.357t.144-.356T9 11.5h6q.213 0 .356.144t.144.357t-.144.356T15 12.5zm12-.5h-1q0-1.27-.904-2.173q-.904-.904-2.17-.904H13.86q-.194 0-.335-.144t-.14-.356q0-.194.143-.347q.144-.153.357-.153h3.038q1.692 0 2.885 1.193T21 12"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const r=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"m18.52 15.758l-.77-.781q1.02-.275 1.635-1.101T20 12q0-1.27-.894-2.173q-.895-.904-2.145-.904h-3.615v-1h3.616q1.67 0 2.854 1.193T21 12q0 1.233-.69 2.23q-.689.999-1.79 1.528M15.311 12.5l-1-1h1.15v1zm5.18 9.408l-18.4-18.4L2.8 2.8l18.4 18.4zm-9.838-5.831H7.077q-1.69 0-2.884-1.193T3 12q0-1.61 1.098-2.777t2.69-1.265h.462l.966.965H7.077q-1.27 0-2.173.904Q4 10.731 4 12t.904 2.173t2.173.904h3.577zM8.539 12.5v-1h2.259l.975 1z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const r=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 6h11M9 12h11M9 18h11M5 6v.01M5 12v.01M5 18v.01"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as t}from"./index-C6LPy0O3.js";const r=e=>t.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...e},t.createElement("path",{fill:"currentColor",d:"M5.616 20q-.691 0-1.153-.462T4 18.384V5.616q0-.691.463-1.153T5.616 4h6.403v1H5.616q-.231 0-.424.192T5 5.616v12.769q0 .23.192.423t.423.192h6.404v1zm10.846-4.461l-.702-.72l2.319-2.319H9.192v-1h8.887l-2.32-2.32l.702-.718L20 12z"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as c}from"./index-C6LPy0O3.js";const l=a=>c.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:40,height:40,viewBox:"0 0 14 14",...a},c.createElement("path",{fill:"currentColor",fillRule:"evenodd",d:"M3.564 4.884a1.91 1.91 0 0 0-1.523.542a1.9 1.9 0 0 0-.442.691a2.5 2.5 0 0 0-.08.511c-.02.255-.02.49 0 .744c.02.241.052.434.08.51A1.87 1.87 0 0 0 2.74 9.015c.263.094.545.129.824.102l.07-.003c.373 0 .78-.2 1.271-.68c.392-.384.77-.879 1.172-1.433c-.401-.554-.78-1.049-1.172-1.432c-.491-.482-.898-.681-1.27-.681l-.071-.003ZM7 8.277a11 11 0 0 1-1.045 1.227c-.598.585-1.352 1.096-2.284 1.109a3.41 3.41 0 0 1-2.687-.974a3.4 3.4 0 0 1-.796-1.246c-.104-.287-.144-.664-.163-.9A6 6 0 0 1 0 7q.001-.247.024-.493c.02-.236.06-.613.163-.9a3.37 3.37 0 0 1 2.048-2.034c.46-.164.95-.227 1.435-.186c.932.013 1.686.524 2.284 1.109c.368.36.716.789 1.045 1.227a11 11 0 0 1 1.045-1.227c.598-.585 1.352-1.096 2.284-1.109a3.41 3.41 0 0 1 2.687.974c.354.352.626.777.796 1.246c.104.287.144.664.163.9q.022.246.025.493q-.002.247-.025.493c-.02.236-.06.613-.163.9a3.37 3.37 0 0 1-2.048 2.034c-.46.164-.95.227-1.435.186c-.932-.013-1.686-.524-2.284-1.109a11 11 0 0 1-1.045-1.227Zm5.48-.905c-.02.241-.051.434-.079.51a1.87 1.87 0 0 1-1.141 1.132a1.9 1.9 0 0 1-.823.102l-.072-.003c-.372 0-.779-.2-1.27-.68c-.392-.384-.77-.879-1.172-1.433c.401-.554.78-1.049 1.172-1.432c.491-.482.898-.681 1.27-.681l.072-.003a1.91 1.91 0 0 1 1.522.542c.197.196.348.432.442.691c.028.077.06.27.08.511c.02.255.02.49 0 .744Z",clipRule:"evenodd"}));export{l as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const r=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("path",{fill:"currentColor",d:"M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5m15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5m-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5s1.5-.675 1.5-1.5s-.675-1.5-1.5-1.5"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const o=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},e.createElement("g",{fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:1.5,color:"currentColor"},e.createElement("path",{d:"m12.88 7.017l4.774 1.271m-5.796 2.525l2.386.636m-2.267 6.517l.954.255c2.7.72 4.05 1.079 5.114.468c1.063-.61 1.425-1.953 2.148-4.637l1.023-3.797c.724-2.685 1.085-4.027.471-5.085s-1.963-1.417-4.664-2.136l-.954-.255c-2.7-.72-4.05-1.079-5.113-.468c-1.064.61-1.426 1.953-2.15 4.637l-1.022 3.797c-.724 2.685-1.086 4.027-.471 5.085c.614 1.057 1.964 1.417 4.664 2.136"}),e.createElement("path",{d:"m12 20.946l-.952.26c-2.694.733-4.04 1.1-5.102.477c-1.06-.622-1.422-1.99-2.143-4.728l-1.021-3.872c-.722-2.737-1.083-4.106-.47-5.184C2.842 6.966 4 7 5.5 7"})));export{o as default};

View File

@ -0,0 +1 @@
import{r as l}from"./index-C6LPy0O3.js";const h=t=>l.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 2048 2048",...t},l.createElement("path",{fill:"currentColor",d:"M1685 768h-326l-80 384h351l-32 128h-333l-113 512H989l111-512H778l-109 512H508l108-512H298l24-128h330l79-384H384l25-128h340l107-512h161L910 640h320l110-512h157l-107 512h323zm-559 384l82-384H886l-85 384z"}));export{h as default};

View File

@ -0,0 +1 @@
import{r as a}from"./index-C6LPy0O3.js";const r=t=>a.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...t},a.createElement("path",{fill:"currentColor",d:"M12 3a2 2 0 1 0 0 4a2 2 0 0 0 0-4m-1 5.874A4.002 4.002 0 0 1 12 1a4 4 0 0 1 1 7.874V11h4a3 3 0 0 1 3 3v1.126A4.002 4.002 0 0 1 19 23a4 4 0 0 1-1-7.874V14a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v1.126A4.002 4.002 0 0 1 5 23a4 4 0 0 1-1-7.874V14a3 3 0 0 1 3-3h4zM19.003 17h-.006a2 2 0 1 0 .006 0M5 17a2 2 0 1 0 0 4a2 2 0 0 0 0-4"}));export{r as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const o=t=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 32 32",...t},e.createElement("g",{fill:"none"},e.createElement("path",{fill:"url(#fluentColorPeople320)",d:"M27.593 18A2.406 2.406 0 0 1 30 20.406S30 25 23 25h.002c-7 0-7-4.594-7-4.594A2.406 2.406 0 0 1 18.408 18z"}),e.createElement("path",{fill:"url(#fluentColorPeople325)",fillOpacity:.5,d:"M27.593 18A2.406 2.406 0 0 1 30 20.406S30 25 23 25h.002c-7 0-7-4.594-7-4.594A2.406 2.406 0 0 1 18.408 18z"}),e.createElement("path",{fill:"url(#fluentColorPeople321)",d:"M5 18a3 3 0 0 0-3 3v.15S2 27 10.5 27s8.5-5.85 8.5-5.85V21a3 3 0 0 0-3-3z"}),e.createElement("path",{fill:"url(#fluentColorPeople322)",d:"M5 18a3 3 0 0 0-3 3v.15S2 27 10.5 27s8.5-5.85 8.5-5.85V21a3 3 0 0 0-3-3z"}),e.createElement("path",{fill:"url(#fluentColorPeople323)",d:"M23 16a4 4 0 1 0 0-8a4 4 0 0 0 0 8"}),e.createElement("path",{fill:"url(#fluentColorPeople324)",d:"M10.5 16a5.5 5.5 0 1 0 0-11a5.5 5.5 0 0 0 0 11"}),e.createElement("defs",null,e.createElement("linearGradient",{id:"fluentColorPeople320",x1:19.331,x2:21.593,y1:18.93,y2:26.153,gradientUnits:"userSpaceOnUse"},e.createElement("stop",{offset:.125,stopColor:"#9c6cfe"}),e.createElement("stop",{offset:1,stopColor:"#7a41dc"})),e.createElement("linearGradient",{id:"fluentColorPeople321",x1:6.043,x2:9.088,y1:19.196,y2:28.383,gradientUnits:"userSpaceOnUse"},e.createElement("stop",{offset:.125,stopColor:"#bd96ff"}),e.createElement("stop",{offset:1,stopColor:"#9c6cfe"})),e.createElement("linearGradient",{id:"fluentColorPeople322",x1:10.5,x2:14.776,y1:16.929,y2:32.021,gradientUnits:"userSpaceOnUse"},e.createElement("stop",{stopColor:"#885edb",stopOpacity:0}),e.createElement("stop",{offset:1,stopColor:"#e362f8"})),e.createElement("linearGradient",{id:"fluentColorPeople323",x1:20.902,x2:24.98,y1:9.063,y2:15.574,gradientUnits:"userSpaceOnUse"},e.createElement("stop",{offset:.125,stopColor:"#9c6cfe"}),e.createElement("stop",{offset:1,stopColor:"#7a41dc"})),e.createElement("linearGradient",{id:"fluentColorPeople324",x1:7.616,x2:13.222,y1:6.462,y2:15.414,gradientUnits:"userSpaceOnUse"},e.createElement("stop",{offset:.125,stopColor:"#bd96ff"}),e.createElement("stop",{offset:1,stopColor:"#9c6cfe"})),e.createElement("radialGradient",{id:"fluentColorPeople325",cx:0,cy:0,r:1,gradientTransform:"matrix(9.21155 -1.02083 .91051 8.21605 14.227 21.5)",gradientUnits:"userSpaceOnUse"},e.createElement("stop",{offset:.392,stopColor:"#3b148a"}),e.createElement("stop",{offset:1,stopColor:"#3b148a",stopOpacity:0})))));export{o as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const t=a=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:40,height:40,viewBox:"0 0 20 20",...a},e.createElement("g",{fill:"currentColor"},e.createElement("g",{opacity:.2},e.createElement("path",{d:"M9.75 7.75a3 3 0 1 1-6 0a3 3 0 0 1 6 0"}),e.createElement("path",{fillRule:"evenodd",d:"M6.75 8.75a1 1 0 1 0 0-2a1 1 0 0 0 0 2m0 2a3 3 0 1 0 0-6a3 3 0 0 0 0 6",clipRule:"evenodd"}),e.createElement("path",{fillRule:"evenodd",d:"M6.8 11.5A1.5 1.5 0 0 0 5.3 13v1.5a1 1 0 0 1-2 0V13a3.5 3.5 0 0 1 7 0v.5a1 1 0 1 1-2 0V13a1.5 1.5 0 0 0-1.5-1.5",clipRule:"evenodd"}),e.createElement("path",{d:"M12.75 7.75a3 3 0 1 0 6 0a3 3 0 0 0-6 0"}),e.createElement("path",{fillRule:"evenodd",d:"M15.75 8.75a1 1 0 1 1 0-2a1 1 0 0 1 0 2m0 2a3 3 0 1 1 0-6a3 3 0 0 1 0 6",clipRule:"evenodd"}),e.createElement("path",{fillRule:"evenodd",d:"M15.7 11.5a1.5 1.5 0 0 1 1.5 1.5v1.5a1 1 0 1 0 2 0V13a3.5 3.5 0 0 0-7 0v.5a1 1 0 1 0 2 0V13a1.5 1.5 0 0 1 1.5-1.5",clipRule:"evenodd"}),e.createElement("path",{fillRule:"evenodd",d:"M11.3 14.25a1.5 1.5 0 0 0-1.5 1.5v1.5a1 1 0 0 1-2 0v-1.5a3.5 3.5 0 0 1 7 0v1.5a1 1 0 1 1-2 0v-1.5a1.5 1.5 0 0 0-1.5-1.5",clipRule:"evenodd"}),e.createElement("path",{d:"M14.25 10.5a3 3 0 1 1-6 0a3 3 0 0 1 6 0"}),e.createElement("path",{fillRule:"evenodd",d:"M11.25 11.5a1 1 0 1 0 0-2a1 1 0 0 0 0 2m0 2a3 3 0 1 0 0-6a3 3 0 0 0 0 6",clipRule:"evenodd"}),e.createElement("path",{d:"M4.25 11.5h5v4h-5zm9 0h5v4h-5z"}),e.createElement("path",{d:"M9.25 13.5h4l.5 4.75h-5z"})),e.createElement("path",{fillRule:"evenodd",d:"M5 9a2 2 0 1 0 0-4a2 2 0 0 0 0 4m0 1a3 3 0 1 0 0-6a3 3 0 0 0 0 6",clipRule:"evenodd"}),e.createElement("path",{fillRule:"evenodd",d:"M3.854 8.896a.5.5 0 0 1 0 .708l-.338.337A3.47 3.47 0 0 0 2.5 12.394v1.856a.5.5 0 1 1-1 0v-1.856a4.47 4.47 0 0 1 1.309-3.16l.337-.338a.5.5 0 0 1 .708 0m11.792-.3a.5.5 0 0 0 0 .708l.338.337A3.47 3.47 0 0 1 17 12.094v2.156a.5.5 0 0 0 1 0v-2.156a4.47 4.47 0 0 0-1.309-3.16l-.337-.338a.5.5 0 0 0-.708 0",clipRule:"evenodd"}),e.createElement("path",{fillRule:"evenodd",d:"M14 9a2 2 0 1 1 0-4a2 2 0 0 1 0 4m0 1a3 3 0 1 1 0-6a3 3 0 0 1 0 6m-4.5 3.25a2.5 2.5 0 0 0-2.5 2.5v1.3a.5.5 0 0 1-1 0v-1.3a3.5 3.5 0 0 1 7 0v1.3a.5.5 0 1 1-1 0v-1.3a2.5 2.5 0 0 0-2.5-2.5",clipRule:"evenodd"}),e.createElement("path",{fillRule:"evenodd",d:"M9.5 11.75a2 2 0 1 0 0-4a2 2 0 0 0 0 4m0 1a3 3 0 1 0 0-6a3 3 0 0 0 0 6",clipRule:"evenodd"})));export{t as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const c=r=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 24 24",...r},e.createElement("g",{fill:"none",stroke:"currentColor",strokeWidth:2},e.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m16.719 19.752l-.64-5.124A3 3 0 0 0 13.101 12h-2.204a3 3 0 0 0-2.976 2.628l-.641 5.124A2 2 0 0 0 9.266 22h5.468a2 2 0 0 0 1.985-2.248"}),e.createElement("circle",{cx:12,cy:5,r:3}),e.createElement("circle",{cx:4,cy:9,r:2}),e.createElement("circle",{cx:20,cy:9,r:2}),e.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M4 14h-.306a2 2 0 0 0-1.973 1.671l-.333 2A2 2 0 0 0 3.361 20H7m13-6h.306a2 2 0 0 1 1.973 1.671l.333 2A2 2 0 0 1 20.639 20H17"})));export{c as default};

View File

@ -0,0 +1 @@
import{r as e}from"./index-C6LPy0O3.js";const o=r=>e.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"1em",height:"1em",viewBox:"0 0 48 48",...r},e.createElement("g",{fill:"none"},e.createElement("path",{fill:"currentColor",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:4,d:"M19 20a7 7 0 1 0 0-14a7 7 0 0 0 0 14M4 40.8V42h30v-1.2c0-4.48 0-6.72-.872-8.432a8 8 0 0 0-3.496-3.496C27.92 28 25.68 28 21.2 28h-4.4c-4.48 0-6.72 0-8.432.872a8 8 0 0 0-3.496 3.496C4 34.08 4 36.32 4 40.8"}),e.createElement("path",{fill:"currentColor",fillRule:"evenodd",d:"M38 13v12zm-6 6h12z",clipRule:"evenodd"}),e.createElement("path",{stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:4,d:"M38 13v12m-6-6h12"})));export{o as default};

Some files were not shown because too many files have changed in this diff Show More