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 中,对 Class 进行代码分割有什么比较优雅的 practice 吗? - 糯米PHP
函数式编程可能是个较好的方案。保证函数的独立性,尽量少依赖类属性,通过函数的组合来实现功能。
(重要)没有通过tag简化数据搜索
比如筛选节点和线、文本标签,都是逐个循环,性能很低。
应该基于tag进行分组,这样就方便多了。
事件交互没设计好
应该按照这样的流程:
1、绑定事件:

2、修改状态:

3、监听状态变化:

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是一个局部函数,我是调用了这个函数的外围函数,每次这个函数在内存中,其实是不一样的内容