Three.js-3D关系可视化

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

(MIT)规范

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

命名

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

关系对象的命名:from_to

实现

CSS2DObject的事件绑定

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

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

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

1
pointerEvents: 'auto';

文本竖排显示

设置css样式即可:

1
writing-mode: 'vertical-lr';

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

1
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方法的地方报错,因为只是定义,并未实现;即使将该方法定义成接口方法也不行):

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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:

1
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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无法删除事件

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

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