• 作者:老汪软件技巧
  • 发表时间:2024-09-10 07:01
  • 浏览量:

本文使用「署名 4.0 国际 (CC BY 4.0)」 许可协议,欢迎转载、或重新修改使用,但需要注明来源。

前文回顾:图形编辑器基础模块以及常见图形算法图形编辑器

一、拓扑图核心元素

二、Topology 架构设计

先简单了解下 byai/topology 是如何使用的。

import { Topology } from '@byai/topology';
const data = {
    lines: [
        {
            start: '1585466878859-0',
            end: '1585466718867',
            color: '#b71522',
        },
    ],
    nodes: [
        {
            id: '1585466878859',
            name: '窄节点',
            content: '这是一个窄节点',
            branches: ['锚点1'],
            position: {
                x: 19726.906692504883,
                y: 19512.21832561493,
            },
        },
        {
            id: '1585466718867',
            name: '宽节点',
            content: '这是一个宽节点',
            branches: ['锚点1', '锚点2', '锚点3'],
            position: {
                x: 19629.79557800293,
                y: 20050.197512626648,
            },
        },
    ],
}
// anchorDecorator: (options: { anchorId?: string }) => (item: ReactNode) => ReactNode;
renderTreeNode = (data: ITopologyNode, { anchorDecorator }: IWrapperOptions) => {
    const {
        name = '',
        content = '',
        branches = [],
    } = data;
    return (
        <div className="topology-node">
            <div className="node-header">{name}div>
            <p className="node-content">{content}p>
            {branches.length > 0 && (
                <div className="flow-node-branches-wrapper">
                    {branches.map(
                        (item: string, index: number) => anchorDecorator({
                            anchorId: `${index}`,
                        })(<div className="flow-node-branch">{item}div>),
                    )}
                div>
            )}
        div>
    );
};
<Topology
    data={data}
    autoLayout
    onChange={this.onChange}
    onSelect={this.handleSelect}
    renderTreeNode={this.renderTreeNode}
    readOnly={readonly}
    getInstance={(ins: any) => { this.topology = ins; }}
/>

MVC、 MVP 、MVVM 架构设计

现有设计:

Byai/Topology 是基于 React 框架实现的图编辑引擎,架构设计上没有完全遵循 MVVM 设计,更多的是 Functional Component Design(功能组件设计),通过组件化思想,将功能拆分成独立组件,利用 React 生命周期做视图更新。 这种设计方式可能较为简单和直观,但同时也存在一些不足之处,随着功能越来越复杂,会对代码可维护性和扩展性有一定挑战。

MVVM 架构:

各个模块之间通过事件解耦。Graph 提供对外 API 能力,API 调用或者 UI 交互都会体现在 Model 层数据的修改,Model 层数据修改后通知 View 层进行视图更新。提供 Registry 能力,将所有可扩展内容注册到注册中心,这些内容可插拔、可自定义,自由极高,可扩展性强。

参考:/antvis/X6/t…

三、功能以及代码分析定位中心

Viewport

在图形学里面,视口代表了一个可看见的多边形区域(通常来说是矩形)。在浏览器范畴里,它代表的是浏览器中网站可见内容的部分。视口外的内容在被滚动进来前都是不可见的。

视口当前可见的部分叫做可视视口(visual viewport)。可视视口可能会比布局视口(layout viewport)更小,因为当用户缩小浏览器缩放比例时,布局视口不变,而可视视口变小了。

getBoundingClientRect

返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。

x, y:表示元素左上角距离视窗的坐标

width, height:表示元素的宽和高

top:元素上边框距离视窗顶部的的距离

bottom:元素下边框距离视窗顶部的的距离

left:元素左边框距离视窗左边的的距离

right:元素右边框距离视窗左部的的距离

scrollTop、scrollLeft

scrollTop:获取或设置元素滚动条到元素顶部的距离。

scrollLeft:获取或设置元素滚动条到元素左边的距离。

实现代码

const getNodeSize = (dom: string | HTMLElement) => {
    if (['string', 'number'].indexOf(typeof dom) > -1) {
        dom = document.getElementById(`dom-map-${dom}`) as HTMLElement;
    }
    if (!dom) {
        return {
            width: 0,
            height: 0,
            left: 0,
            top: 0,
        } as ClientRect;
    }
    return (dom as HTMLElement).getBoundingClientRect();
};
const computeMaxAndMin = (nodes: ITopologyNode[]) => {
    if (!nodes.length || nodes.find(item => !item.position || [item.position.x, item.position.y].includes(undefined))) {
        return null;
    }
    let minX = Infinity;
    let maxX = -Infinity;
    let minY = Infinity;
    let maxY = -Infinity;
    nodes.forEach(({ position, id }) => {
        const nodeSize = getNodeSize(id);
        const { x, y } = position;
        minX = Math.min(minX, x);
        maxX = Math.max(maxX, x + nodeSize.width);
        minY = Math.min(minY, y);
        maxY = Math.max(maxY, y + nodeSize.height);
    });
    return {
        minX,
        maxX,
        minY,
        maxY
    };
};
const computeContentCenter = (nodes: ITopologyNode[]) => {
    if (!computeMaxAndMin(nodes)) return null;
    const {
        minX, maxX, minY, maxY
    } = computeMaxAndMin(nodes);
    return {
        x: (minX + maxX) / 2,
        y: (minY + maxY) / 2,
    };
};
const scrollCanvasToCenter = () => {
    if (!this.$wrapper || !this.$canvas) {
        return;
    }
    const canvasSize = getNodeSize(this.$canvas);
    const wrapperSize = getNodeSize(this.$wrapper);
    const contentCenter = computeContentCenter(this.props.data.nodes);
    const canvasCenter = {
        x: canvasSize.width / 2,
        y: canvasSize.height / 2
    };
    const defaultScrollTop = (canvasSize.height - wrapperSize.height) / 2;
    const defaultScrollLeft = (canvasSize.width - wrapperSize.width) / 2;
    if (!contentCenter) {
        this.$wrapper.scrollTop = defaultScrollTop;
        this.$wrapper.scrollLeft = defaultScrollLeft;
    } else {
        this.$wrapper.scrollTop = defaultScrollTop + (contentCenter.y - canvasCenter.y);
        this.$wrapper.scrollLeft = defaultScrollLeft + (contentCenter.x - canvasCenter.x);
    }
};

进阶:画布缩放之后如何计算呢?

核心:计算缩放之后节点中心坐标(运用缩放、平移矩阵),改写 computeMaxAndMin 方法

const computeCanvasPoHelper = ( $wrapper: HTMLDivElement) => {
    // 缩放的容器
    const canvas = document.querySelector('.topology-canvas');
    const { width, height } = canvas.getBoundingClientRect();
    const zoom = parseInt(document.querySelector('.topology-tools-percent').innerHTML) / 100;
    // 缩放后画布的中心点,还是需要用缩放前的比例计算中心点
    const centerX = width / zoom / 2;
    const centerY = height / zoom / 2;
    return {
        centerX,
        centerY,
        zoom,
    };
};
export const computeMaxAndMin = (nodes: ITopologyNode[], $wrapper) => {
    if (!nodes.length || nodes.find(item => !item.position || [item.position.x, item.position.y].includes(undefined))) {
        return null;
    }
    let minX = Infinity;
    let maxX = -Infinity;
    let minY = Infinity;
    let maxY = -Infinity;
    const {
        centerX, centerY, zoom
    } = computeCanvasPoHelper( $wrapper);
    nodes.forEach(({ position, id }) => {
        const nodeSize = getNodeSize(id);
        const { x, y } = position;
        minX = Math.min(minX, (x - centerX) * zoom + centerX);
        maxX = Math.max(maxX, (x - centerX) * zoom + centerX + nodeSize.width * zoom);
        minY = Math.min(minY, (y - centerY) * zoom + centerY);
        maxY = Math.max(maxY, (y - centerY) * zoom + centerY + nodeSize.height * zoom);
    });
    return {
        minX,
        maxX,
        minY,
        maxY
    };
};

框选

包围体

在计算机图形学与计算几何领域,一组物体的包围体就是将物体组合完全包容起来的一个封闭空间。将复杂物体封装在简单的包围体中,就可以提高几何运算的效率。通常简单的物体比较容易检查相互之间的重叠。一组物体的包围体也是包含一个物体及周围相关环境的封闭空间,因此可以用它来表示一个非空、有限的单一物体。

应用场景

在光线跟踪中,包围体用于光线相交检验,在许多渲染算法中,它又用于视体的检验。如果光线或者视体与包围体没有交叉,那么就不会与包围体内的物体相交。通过这样的相交检验,就可以生成需要显示的物体列表。在碰撞检测中,如果两个包围体没有相交,那么所包含的物体也就不会碰撞。

常见类型

R 树(R-tree)

假设有一张地图,上面有几百万个节点,要快速找某个位置半径 2 公里的所有学校的信息。

低效的做法是遍历这几百万的节点的位置,判断距离是否小于 2 公里。

但如果用上索引技术,比如 R 树,我们就能利用索引去空间换时间,快速拿到特定范围的节点超集,比如几千个。

接着只需要遍历这几千个节点去判断符合条件的节点就可以了,而不需要完完整整遍历所有的节点。

一种空间索引技术,能够是从大量的节点中,快速找到特定范围的元素集合,而不用一个不落地遍历所有节点。

R 树的叶子节点是数据节点,保存有图形信息和它的最小包围矩形(MBR)。

R 树会将距离相近的数据节点收集起来,并放到同一个父节点下。这个父节点是索引节点,不会保存图形信息,但会记录子节点的合并的包围盒数据,父节点如果多了,也会把它们收集起来,放到一个新的父节点下。

{
  minX: 20,
  minY: 40,
  maxX: 30,
  maxY: 50,
  // 保存图形数据,比如图形对象 id,或图形对象本身
  data: {}
};

/mourner/rbu…

R 数检索的过程如下:

提供一个选区矩形,从根节点开始,往下递归查找判断选取矩形是否和当前节点矩形相交。

若不相交,其下的节点也不会相交,该节点对应的子树就不需要继续递归了;若相交且为数据节点(叶子节点),将其放到 result 数组;若是包含关系,其下的所有数据节点放到 result 数组;若相交但并不包含,则遍历其下的子节点,重复前面的操作。

直到可能相交的节点遍历完结束,然后返回 result 数组。

矩形包含检测

interface Bbox {
  minXnumber;
  minYnumber;
  maxXnumber;
  maxYnumber;
}
var rect1 = {x: 5, y: 5, width: 50, height: 50}
var rect2 = {x: 20, y: 10, width: 10, height: 10}
maxX = x + width;
maxY = y + height;
function isRectContain2(rect1: IBox2, rect2: IBox2) {
  return (
    rect1.minX <= rect2.minX &&
    rect1.minY <= rect2.minY &&
    rect1.maxX >= rect2.maxX &&
    rect1.maxY >= rect2.maxY
  );
}

计算最小外接矩形

function getRectsBBox(...rects: IRect[]): IBox {
  const minX = Math.min(...rects.map((rect) => rect.x));
  const minY = Math.min(...rects.map((rect) => rect.y));
  const maxX = Math.max(...rects.map((rect) => rect.x + rect.width));
  const maxY = Math.max(...rects.map((rect) => rect.y + rect.height));
  return {
    x: minX,
    y: minY,
    width: maxX - minX,
    height: maxY - minY,
  };
}

树形布局和 Dagre 布局(可先初步阅读,待补充)

/phenomLi/Bl…

/p/346059370

/post/710148…

四、未来方向节点转换节点编组标尺性能优化MVVM 架构dnd 升级