搜索文章

输入关键词开始搜索

Three.js-3D关系可视化

Three.js#Three.js#3D#数据可视化#程序设计#算法#数据结构

记录项目中的一些想法和经验教训。

(MIT)规范

用户数据必须挂在3D对象的userData属性上。

命名

特别要注意3D对象的id属性的命名

关系对象的命名:from_to

实现

CSS2DObject的事件绑定

这次的文本就是用的CSS2DObject绘制的,事件绑定需要这样处理:

labelCSS2DObject.element.addEventListener('click', event => {
  this.triggerClick(label.name);
});

注意,需要给每一个CSS2DObject对象增加如下CSS属性,否则无法触发其鼠标交互事件:

pointerEvents: 'auto';

文本竖排显示

设置css样式即可:

writing-mode: 'vertical-lr';

竖排后,如果想要精确计算其高宽,得延时通过包围盒获取才行:

element.getBoundingClientRect();

数据解析

程序分为配置文件+解析函数+调用流程

配置文件单独放一个文件。

解析函数单独放一个文件,可以作为通用的工具函数。

调用流程单独放一个文件。

选择一种合适的经典数据结构

比如Node+Edge,还是Tree?先思考好。

后续的操作都是围绕这个展开的。

(TODO)做一次可视化数据结构的分享

程序设计

应该设计若干个Node类

1、解决Node.isNull()这类问题。

2、不同的Node渲染效果、类型不一样(Sprint、SphereGeometry)

MVC还是最经典的

model: 数据处理

view: 渲染

action: 事件

没有类似的设计,代码写成了一大团乱麻。

果然经典还是永流传的。

渲染分离,可重复调用

我之前没弄好这个,结果导致做2D视角时,难以重复调用render函数来解决。

参考C++的模式:方法定义和方法实现分离开

比如HierarchyLineLayout,其方法定义放在HierarchyLineLayout.ts中(不对,这里先定义了,会导致该文件中调用getPositionByChildrenNum方法的地方报错,因为只是定义,并未实现;即使将该方法定义成接口方法也不行):

  /**
   * 根据子元素的数量来计算节点的位置
   */
  getPositionByChildrenNumber!: (nodes: Node[], type: string) => void;

具体的实现放在ViewHelper.ts中:

import HierarchyLineLayout from './HierarchyLineLayout';
import { Node } from '@/type/Data';

HierarchyLineLayout.prototype.getPositionByChildrenNumber = function (nodes: Node[], type: string) {
  let totalChildrenNumber = 0;
  nodes.forEach(node => {
    totalChildrenNumber += node.value;
  });

  // 计算弧度
  let startAngle = 0;
  const config = this.getConfigByType(type);

  nodes.forEach(node => {
    const angle = (node.value / totalChildrenNumber) * Math.PI * 2;
    node.startAngle = startAngle;
    node.endAngle = startAngle + angle;
    startAngle += angle;

    const halfAngle = node.startAngle + angle / 2;
    const x = config.layout.radius * Math.cos(halfAngle);
    const z = config.layout.radius * Math.sin(halfAngle);

    const position: [number, number, number] = [x, config.layout.y, z];
    node.position = position;
  });
};

export { HierarchyLineLayout };

使用该类时,不再引入HierarchyLineLayout,而是引入ViewHelper:

import { HierarchyLineLayout } from '@/layout/ViewHelper';

这样就可以解决多人合作写一个类,代码冲突的问题;也能解决单个类文件过大的问题。

为什么这种方式不行?

因为顺序的缘故:

  • 定义HierarchyLineLayout类

  • 通过prototype扩展了类HierarchyLineLayout,给其加上了getPositionByChildrenNumber()方法

  • 使用时,实例化了HierarchyLineLayout类

  • HierarchyLineLayout类里面,又重新定义了getPositionByChildrenNumber属性,且这里没有给其赋值,因此该方法为undefined,导致报错

所以HierarchyLineLayout类文件里面,不能定义getPositionByChildrenNumber()方法。

这样导致的后果,就是扩展文件ViewHelper.ts中有很多红线提示。

一些拆分方案

如何将TypeScript类拆分为多个文件? |

TypeScript 中,对 Class 进行代码分割有什么比较优雅的 practice 吗? - 糯米PHP

函数式编程可能是个较好的方案。保证函数的独立性,尽量少依赖类属性,通过函数的组合来实现功能。

(重要)没有通过tag简化数据搜索

比如筛选节点和线、文本标签,都是逐个循环,性能很低。

应该基于tag进行分组,这样就方便多了。

事件交互没设计好

应该按照这样的流程:

1、绑定事件:

1

2、修改状态:

2

3、监听状态变化:

3

4、执行逻辑:

4

算法

递归思想

比如我高亮时获取关联节点。

善用数据结构,能极大提升效率和程序质量。

分工

可以按照数据处理、绘图、事件、动画、特效进行分工。

大家对于数据的理解保持一致即可,这是核心。

方案

旋转相机,物体相对视线的位置不变

注意:目前的方案其实是一个不够强大的方案,是建立在视角中心固定不变、y轴不变的情况下的(即场景只能水平旋转,不能上下旋转和平移)。

思路是:

1、根据相机的当前位置和初始位置,计算相机转动的角度;

2、根据这个角度和三角函数、物体的初始位置,计算物体的当前位置

这里有一个要注意的地方:camera的x为负数时没问题,x为正数时则会跟着相机旋转,需要将角度乘以-1

  const { x, y, z } = camera.position;
  const vector = new Vector3(x, y, z);
  let angle = vector.angleTo(defaultVector);
  // camera的x为负数时没问题,x为正数时则会跟着相机旋转,需要处理下
  if (camera.position.x > 0) {
    angle *= -1;
  }

  // 节点和文本不再加入根据距离显示opacity的功能
  specialNodes.forEach(node => {
    const defaultNodePosition = node.position;
    const x1 = Math.cos(angle) * defaultNodePosition[0];
    const z1 = Math.sin(angle) * defaultNodePosition[0];

    const position: [number, number, number] = [x1, defaultNodePosition[1], z1];
    changeObjectsByNode(node, position, enableDepthOfField, layout);
  });

问题

(TODO)removeEventListener无法删除事件

if (type === '2D') {
    orbitControls.addEventListener('change', changePosition);
} else {
    // TODO:无法删除
    orbitControls.removeEventListener('change', changePosition);
    // changePosition = () => {};
}

这是因为changePosition是一个局部函数,我是调用了这个函数的外围函数,每次这个函数在内存中,其实是不一样的内容