vue2-mind-map
Version:
`vue2-mind-map` 是一个功能丰富的思维导图可视化组件。该组件提供了完整的思维导图展示和交互功能,支持多种数据格式输入、灵活的布局配置、丰富的样式定制和流畅的用户交互体验。目前基于 Vue 2 构建。
1,508 lines (1,496 loc) • 273 kB
JavaScript
var MindMap = (function (exports, Vue) {
'use strict';
/**
* 表示二维空间中的矢量
*/
class Vector {
/**
* 矢量的 x 坐标
*/
x;
/**
* 矢量的 y 坐标
*/
y;
/**
* 创建一个新的矢量
* @param x - x 坐标值
* @param y - y 坐标值
*/
constructor(x, y) {
this.x = x;
this.y = y;
}
/**
* 将当前矢量与另一个矢量相加
* @param vector - 要添加的矢量
* @returns 相加后的新矢量
*/
add(vector) {
return new Vector(this.x + vector.x, this.y + vector.y);
}
/**
* 将当前矢量乘以标量
* @param scalar - 乘数
* @returns 缩放后的新矢量
*/
multiply(scalar) {
return new Vector(this.x * scalar, this.y * scalar);
}
/**
* 判断当前矢量是否与另一个矢量相等
* @param vector - 要比较的矢量
* @returns 如果两个矢量的 x 和 y 坐标相等,则返回 true,否则返回 false
*/
equals(vector) {
return this.x === vector.x && this.y === vector.y;
}
/**
* 创建当前矢量的副本
* @returns 当前矢量的一个新副本
*/
clone() {
return new Vector(this.x, this.y);
}
}
/*
* @Author: Billy
* @Date: 2020-09-20 01:12:40
* @LastEditors: Billy
* @LastEditTime: 2023-06-08 17:28:20
* @Description: 加密功能函数集
*/
/**
* 生成uuid/guid
* @see https://stackoverflow.com/questions/105034/how-to-create-guid-uuid
* @returns {string} uuid 字符串
*/
function uuidv4() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0, v = c == "x" ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const DEFAULT_SIZE = new Vector(100, 100);
class MindNode {
id; // 节点ID
parent; // 父节点
children = []; // 子节点集合
position; // 位置
size; // 尺寸
collapsed; // 节点是否折叠
totalChildrenSize; // 子节点总尺寸(用于布局计算)
totalChildrenCount; // 子节点总数量
data = null; // 存储节点自定义数据
_content; // 节点内容
rgTf = {
depth: 0,
prelim: 0,
mod: 0,
shift: 0,
change: 0,
thread: undefined,
ancestor: undefined,
lmost_sibling: undefined,
number: 0 // 在同级节点中的索引 (0-based)
};
slDv = {
maxSubtreeSpan: 0,
isSizeLargerThanSubtreeSpan: false,
excessSizeOverSubtreeSpan: 0, // 本身高度大于下级的占位的高度差
};
get width() { return this.size.x; }
set width(value) { this.size.x = value; }
get height() { return this.size.y; }
set height(value) { this.size.y = value; }
get x() { return this.position.x; }
set x(value) { this.position.x = value; }
get y() { return this.position.y; }
set y(value) { this.position.y = value; }
get content() {
return this._content;
}
set content(value) {
this._content = value;
}
/**
* 创建一个思维导图节点实例
*
* @param options - 节点配置选项
* @param options.id - 节点唯一标识,默认自动生成UUID
* @param options.position - 节点位置向量,默认为(0,0)
* @param options.size - 节点尺寸向量,默认为(100,100)
* @param options.collapsed - 是否折叠节点,默认为false
* @param children - 子节点列表,默认为空数组
*/
constructor(options = {}, children = []) {
const { id = uuidv4(), position = new Vector(0, 0), size = DEFAULT_SIZE.clone(), collapsed = false // 默认为不折叠
} = options;
this.id = id;
this.position = position;
this.size = size;
this.collapsed = collapsed;
this.children = children;
this.children.forEach(child => {
child.parent = this;
});
}
// 添加子节点
addChild(node) {
// 防止添加自身
if (node === this) {
throw new Error("Cannot add self as child");
}
// 防止添加祖先节点(避免循环引用)
let parent = this.parent;
while (parent) {
if (parent === node) {
throw new Error("Cannot add ancestor as child");
}
parent = parent.parent;
}
if (node.parent) {
node.parent.removeChild(node);
}
this.children.push(node);
node.parent = this;
}
// 移除子节点
removeChild(node) {
const index = this.children.indexOf(node);
if (index !== -1) {
this.children.splice(index, 1);
node.parent = undefined;
}
}
/**
* 深度优先遍历节点树
* @param callback - 对每个遍历到的节点执行的回调函数
* @param {boolean} [skipCollapsedSubtrees=false] - 可选参数,如果为 true,则当遇到折叠的节点时,不会遍历其子树。默认为 false,即遍历所有节点。
*/
traverse(callback, skipCollapsedSubtrees = false) {
callback(this); // 总是对当前节点执行回调
// 如果设置了 skipCollapsedSubtrees 为 true 且当前节点已折叠,则不继续遍历其子节点
if (skipCollapsedSubtrees && this.collapsed) {
return;
}
for (const child of this.children) {
// 递归调用时传递 skipCollapsedSubtrees 参数
child.traverse(callback, skipCollapsedSubtrees);
}
}
// 查找方法
findById(id) {
if (this.id === id)
return this;
for (const child of this.children) {
const found = child.findById(id);
if (found)
return found;
}
return null;
}
// 移动整个子树
moveTo(newParent) {
if (this.parent) {
this.parent.removeChild(this);
}
newParent.addChild(this);
}
}
// Generated by Claude 3.7 Sonnet Thinking
/**
* 渲染管理器 - 用于统一管理渲染流程和优化
*/
class RenderManager {
// 单例实例
static instance;
/**
* 渲染队列 - 存储需要渲染的节点
* @remark
* 使用 Set 来存储节点,保证每个节点在队列中只存在一次,避免重复渲染
*/
renderQueue = new Set();
// 动画帧 ID - 用于 requestAnimationFrame
animationFrameId;
// 是否正在渲染 - 用于避免重复渲染
isRendering = false;
/**
* 获取单例实例
* @returns RenderManager 实例
*/
static getInstance() {
if (!RenderManager.instance) {
RenderManager.instance = new RenderManager();
}
return RenderManager.instance;
}
constructor() {
// 私有构造函数,防止外部直接创建实例
}
/**
* 将节点添加到渲染队列
* @param node 需要渲染的节点
*/
queueForRender(node) {
this.renderQueue.add(node);
this.scheduleRender();
}
/**
* 批量将节点添加到渲染队列
* @param nodes 需要渲染的节点数组
*/
queueMultiple(nodes) {
nodes.forEach(node => this.renderQueue.add(node));
this.scheduleRender();
}
/**
* 从渲染队列中移除节点
* @param node 要移除的节点
*/
unqueueFromRender(node) {
this.renderQueue.delete(node);
}
/**
* 安排渲染(使用requestAnimationFrame进行优化)
*/
scheduleRender() {
if (!this.animationFrameId && !this.isRendering) {
this.animationFrameId = requestAnimationFrame(() => this.render());
}
}
/**
* 执行实际渲染
*/
render() {
this.animationFrameId = undefined;
this.isRendering = true;
// 按照层级排序节点,确保父节点先于子节点渲染(level越小越先渲染)
const sortedNodes = Array.from(this.renderQueue).sort((a, b) => a.level - b.level);
// 执行渲染
sortedNodes.forEach(node => {
if (this.renderQueue.has(node)) {
node.render();
this.renderQueue.delete(node);
}
});
this.isRendering = false;
// 在渲染过程中,如果有新的节点加入队列,则重新安排渲染
if (this.renderQueue.size > 0) {
this.scheduleRender();
}
}
/**
* 立即渲染指定节点(不经过队列)
* @param node 要立即渲染的节点
*/
renderImmediate(node) {
node.render();
}
/**
* 立即渲染所有队列中的节点
*/
flushRender() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = undefined;
}
this.render();
}
/**
* 清空渲染队列
*/
clearQueue() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = undefined;
}
this.renderQueue.clear();
}
/**
* 管理容器 - 将容器添加到渲染队列
* @param container 要管理的容器
*/
manageContainer(container) {
// 将容器添加到渲染队列
this.queueForRender(container);
}
/**
* 批量渲染容器内的所有节点
* @param container 容器
*/
renderContainer(container) {
const allNodes = this.collectAllNodes(container);
this.queueMultiple(allNodes);
this.flushRender();
}
/**
* 收集容器中的所有节点(包括嵌套容器中的节点)
* @param container 容器
* @returns 所有节点数组
*/
collectAllNodes(container) {
const result = [];
this.traverseNodes(container, result);
return result;
}
/**
* 递归遍历节点并添加到结果数组
* @param node 当前节点
* @param result 结果数组
*/
traverseNodes(node, result) {
result.push(node); // 先添加当前节点
// 避免循环引用,不使用 node instanceof Container 这种判断方式
if (this.isTraversable(node)) {
// 如果是容器,则遍历其子节点
node.getChildren().forEach(child => {
this.traverseNodes(child, result); // 递归处理子节点
});
}
}
/**
* 类型守卫:检查节点是否可遍历
* @param node 要检查的节点
* @returns 如果节点实现了 ITraversable 则返回 true
*/
isTraversable(node) {
return node && typeof node.getChildren === 'function';
// return node && ('getChildren' in node) && typeof node.getChildren === 'function';
}
}
/**
* 事件发射器基类
* 提供事件注册、移除和触发的基本功能。
* 通过泛型 M (事件映射表) 来实现事件名称和事件数据的类型安全。
* @template M 一个映射表,其键是事件名称 (字符串),其值是该事件对应的数据类型。
* 默认为 BaseEventMap,允许任何字符串事件名和 any 类型的数据。
*/
class EventEmitter {
// 事件监听器映射。
// 内部存储使用 string 作为键,值为对应事件处理函数数组。
// 类型安全通过公共方法签名来保证。
_eventListeners = new Map();
/**
* 添加事件监听器。
* @param eventName 事件类型,必须是 M 中的一个键。
* @param handler 事件处理函数,它接收特定于此事件类型的数据 (M[K])。
*/
on(eventName, handler) {
const eventKey = eventName; // K 是 M 的键,M 继承自 Record<string, any>,所以 K 是字符串兼容的
if (!this._eventListeners.has(eventKey)) {
this._eventListeners.set(eventKey, []);
}
// 将具体类型的 handler (EventHandler<M[K]>) 存入 EventHandler<any> 数组是安全的(向上转型)
this._eventListeners.get(eventKey).push(handler);
return this;
}
/**
* 添加只执行一次的事件监听器。
* @param eventName 事件类型,必须是 M 中的一个键。
* @param handler 事件处理函数,接收 M[K] 类型的数据。
*/
once(eventName, handler) {
// 包装后的 onceHandler 保持了与原始 handler 相同的类型签名 EventHandler<M[K]>
const onceHandler = (eventData) => {
this.off(eventName, onceHandler); // 移除包装后的 handler
handler(eventData);
};
// 将原始处理函数附加到包装器上,以便 off 方法能够通过它来移除监听器
onceHandler._originalHandler = handler;
this.on(eventName, onceHandler); // on 方法会正确处理类型
return this;
}
/**
* 移除事件监听器。
* @param eventName 事件类型,必须是 M 中的一个键。
* @param handler 事件处理函数。
*/
off(eventName, handler) {
const eventKey = eventName;
if (!this._eventListeners.has(eventKey))
return this;
const handlers = this._eventListeners.get(eventKey);
// 需要查找原始处理函数(如果是通过 once 添加的)或处理函数本身
const index = handlers.findIndex(h => h === handler || h._originalHandler === handler);
if (index !== -1) {
handlers.splice(index, 1);
}
// 如果移除后没有处理函数了,删除该事件类型
if (handlers.length === 0) {
this._eventListeners.delete(eventKey);
}
return this;
}
/**
* 触发事件。
* @param eventName 事件类型,必须是 M 中的一个键。
* @param eventData 事件数据,其类型应为 M[K]。
*/
emit(eventName, eventData) {
const eventKey = eventName;
if (!this._eventListeners.has(eventKey))
return;
// 复制一份处理函数数组,防止在回调中修改监听器列表导致问题
// 从 _eventListeners 中获取的 handlers 数组元素类型是 EventHandler<any>
// 但我们知道对于特定的 eventName K,它们实际上是 EventHandler<M[K]> (由 on 方法保证)
// 因此,使用 M[K] 类型的 eventData 调用它们是类型安全的
const handlersToExecute = [...this._eventListeners.get(eventKey)];
handlersToExecute.forEach(handler => handler(eventData));
}
/**
* 移除指定类型的所有事件监听器。
* @param eventName 事件类型 (M 中的键),如果不提供则移除所有类型的监听器。
*/
removeAllListeners(eventName) {
if (eventName) {
const eventKey = eventName;
this._eventListeners.delete(eventKey);
}
else {
this._eventListeners.clear();
}
return this;
}
}
class Matrix {
data;
/**
* 创建一个新的矩阵对象
* 默认初始化为单位矩阵
*/
constructor() {
this.data = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
];
}
/**
* 创建一个新的单位矩阵
* @returns 新的单位矩阵实例
*/
static identity() {
return new Matrix();
}
/**
* 创建一个平移变换矩阵
* @param x - X轴平移距离
* @param y - Y轴平移距离
* @returns 新的平移变换矩阵
*/
static translation(x, y) {
const m = new Matrix();
m.data[0][2] = x;
m.data[1][2] = y;
return m;
}
/**
* 创建一个旋转变换矩阵
* @param angle - 旋转角度(弧度制)
* @returns 新的旋转变换矩阵
*/
static rotation(angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const m = new Matrix();
m.data[0][0] = cos;
m.data[0][1] = -sin;
m.data[1][0] = sin;
m.data[1][1] = cos;
return m;
}
/**
* 创建一个缩放变换矩阵
* @param sx - X轴方向的缩放因子
* @param sy - Y轴方向的缩放因子
* @returns 新的缩放变换矩阵
*/
static scaling(sx, sy) {
const m = new Matrix();
m.data[0][0] = sx;
m.data[1][1] = sy;
return m;
}
/**
* 将当前矩阵与另一个矩阵相乘
* 计算 this * other 的结果
* @param other - 要相乘的另一个矩阵
* @returns 矩阵乘法的结果矩阵
*/
multiply(other) {
const result = new Matrix();
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
result.data[i][j] = 0;
for (let k = 0; k < 3; k++) {
result.data[i][j] += this.data[i][k] * other.data[k][j];
}
}
}
return result;
}
/**
* 创建当前矩阵的深拷贝
* @returns 当前矩阵的副本
*/
clone() {
const result = new Matrix();
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
result.data[i][j] = this.data[i][j];
}
}
return result;
}
/**
* 使用当前矩阵变换一个点
* @param x - 点的 x 坐标
* @param y - 点的 y 坐标
* @returns 变换后的点坐标
*/
transformPoint(x, y) {
const newX = this.data[0][0] * x + this.data[0][1] * y + this.data[0][2];
const newY = this.data[1][0] * x + this.data[1][1] * y + this.data[1][2];
return { x: newX, y: newY };
}
}
class Style {
// 字体属性
fontFamily;
fontSize;
fontStyle;
fontWeight;
// 颜色属性
color;
// 边框属性
borderWidth;
borderColor;
borderStyle;
// 背景属性
backgroundColor;
}
/**
* 节点基类
*/
class Node extends EventEmitter {
/** 节点 ID */
id;
// 用于生成唯一节点 ID 的静态计数器
static _nextNodeId = 0;
// 父节点引用
_parent;
// 添加层级属性,默认为0(根节点)
_level = 0;
// 样式
_style = new Style();
// 局部变换
_localMatrix = new Matrix();
// 全局变换(缓存子节点的全局变换矩阵)
_globalMatrix = new Matrix();
// 当前缩放级别
_zoomLevel = 0;
// 基础缩放因子 (例如 1.1 表示每次缩放10%)
_zoomFactorBase;
/**
* Node 构造函数
* @param options 可选的构造参数对象
*/
constructor(options) {
super(); // EventEmitter constructor
const { id, zoomFactorBase = 1.1 } = options || {};
if (id) {
this.id = id;
}
else {
// 使用当前类的名称(小写)作为前缀
this.id = `${this.constructor.name.toLowerCase()}-${Node._nextNodeId++}`;
}
this._zoomFactorBase = zoomFactorBase;
}
/** 获取父节点 */
get parent() {
return this._parent;
}
/** 设置父节点 */
set parent(value) {
this._parent = value;
this.updateLevel(); // 更新层级
}
/** 获取节点层级 */
get level() { return this._level; }
/** 获取节点样式 */
get style() { return this._style; }
/** 获取局部变换矩阵的克隆副本 */
get localMatrix() {
return this._localMatrix.clone();
}
/** 获取全局变换矩阵的克隆副本 */
get globalMatrix() {
return this._globalMatrix.clone();
}
/** 节点的局部坐标 */
get position() {
return new Vector(this._localMatrix.data[0][2], this._localMatrix.data[1][2]);
}
/** 节点的局部缩放比例 */
get scaling() {
return new Vector(this._localMatrix.data[0][0], this._localMatrix.data[1][1]);
}
/** 节点的局部旋转角度 */
get rotation() {
return Math.atan2(this._localMatrix.data[1][0], this._localMatrix.data[0][0]);
}
/** 节点的全局坐标 */
get globalPosition() {
return new Vector(this._globalMatrix.data[0][2], this._globalMatrix.data[1][2]);
}
/** 节点的全局缩放比例 */
get globalScaling() {
return new Vector(this._globalMatrix.data[0][0], this._globalMatrix.data[1][1]);
}
/** 节点的全局旋转角度 */
get globalRotation() {
return Math.atan2(this._globalMatrix.data[1][0], this._globalMatrix.data[0][0]);
}
/** 获取当前缩放级别 */
get zoomLevel() {
return this._zoomLevel;
}
/** 获取基础缩放因子 */
get zoomFactorBase() {
return this._zoomFactorBase;
}
/**
* 更新当前节点的层级
* @remarks
* 根据父节点层级计算自身层级,如无父节点则为0级(根节点)
*/
updateLevel() {
this._level = this._parent ? this._parent.level + 1 : 0;
}
// ------------------ 实现 IStyleable 接口 ------------------
/**
* 应用字体样式
* @param fontStyle - 要应用的字体样式对象
*/
applyFont(fontStyle) {
this.style.fontFamily = fontStyle.fontFamily;
this.style.fontSize = fontStyle.fontSize;
this.style.fontStyle = fontStyle.fontStyle;
this.style.fontWeight = fontStyle.fontWeight;
}
/**
* 应用颜色样式
* @param colorStyle - 要应用的颜色样式对象
*/
applyColor(colorStyle) {
this.style.color = colorStyle.color;
}
/**
* 应用边框样式
* @param borderStyle - 要应用的边框样式对象
*/
applyBorder(borderStyle) {
this.style.borderWidth = borderStyle.borderWidth;
this.style.borderColor = borderStyle.borderColor;
this.style.borderStyle = borderStyle.borderStyle;
}
/**
* 应用背景样式
* @param backgroundStyle - 要应用的背景样式对象
*/
applyBackground(backgroundStyle) {
this.style.backgroundColor = backgroundStyle.backgroundColor;
}
// ------------------ 实现 ITransformable 接口 ------------------
/**
* 更新全局变换矩阵
*
* @remarks
* 如果存在父节点,全局变换矩阵是父节点全局变换与自身局部变换的组合
* 如果是根节点,全局变换矩阵等同于局部变换矩阵
*/
updateGlobalTransform() {
if (this._parent) {
// 如果有父节点,全局变换 = 父节点全局变换 × 自身局部变换
this._globalMatrix = this._parent._globalMatrix.multiply(this._localMatrix);
}
else {
// 如果是根节点,全局变换等于局部变换
this._globalMatrix = this._localMatrix.clone();
}
}
/**
* 将节点标记为需要渲染,并加入渲染队列
*/
queueRender() {
RenderManager.getInstance().queueForRender(this);
}
/**
* 按指定的 x 和 y 距离移动节点
* @param x - 节点沿 x 轴移动的距离
* @param y - 节点沿 Y 轴移动的距离
* @returns 用于方法链的节点实例
*/
move(x, y) {
const translation = Matrix.translation(x, y);
this._localMatrix = translation.multiply(this._localMatrix);
this.updateGlobalTransform();
this.queueRender();
return this;
}
/**
* 按指定角度旋转节点
* @param angle - 以弧度为单位的旋转角度
* @param cx - 旋转中心的 x 坐标(默认值:0)
* @param cy - 旋转中心的 Y 坐标(默认值:0)
* @returns 实例本身,允许方法链
*
* @remarks
* 如果 cx 和 cy 均为 0,则旋转围绕原点进行。
* 否则,将围绕指定点 (cx, cy) 旋转。
* 旋转后,全局变换矩阵会自动更新。
*/
rotate(angle, cx = 0, cy = 0) {
if (cx === 0 && cy === 0) {
// 围绕原点旋转
const rotation = Matrix.rotation(angle);
this._localMatrix = rotation.multiply(this._localMatrix);
}
else {
// 围绕指定点 (cx, cy) 旋转
const translateToOrigin = Matrix.translation(-cx, -cy);
const rotation = Matrix.rotation(angle);
const translateBack = Matrix.translation(cx, cy);
// 组合矩阵:translateBack × rotation × translateToOrigin
const combined = translateBack.multiply(rotation).multiply(translateToOrigin);
this._localMatrix = combined.multiply(this._localMatrix);
}
this.updateGlobalTransform();
this.queueRender();
return this;
}
/**
* 沿 X 轴和 Y 轴以指定系数缩放节点
* @param sx - 沿 X 轴的缩放因子
* @param sy - 沿 Y 轴的缩放因子
* @param cx - 缩放中心的 x 坐标(默认值:0)
* @param cy - 缩放中心的 Y 坐标(默认值:0)
* @returns 用于方法链的节点实例
*
* @example
* // 围绕原点水平缩放 2 倍
* block.scale(2, 1);
*
* @example
* // 在点(100,100)周围的两个维度上均缩放 0.5 倍
* block.scale(0.5, 0.5, 100, 100);
*/
scale(sx, sy, cx = 0, cy = 0) {
if (cx === 0 && cy === 0) {
// 围绕原点缩放
const scaling = Matrix.scaling(sx, sy);
this._localMatrix = scaling.multiply(this._localMatrix);
}
else {
// 围绕指定点 (cx, cy) 缩放
const translateToOrigin = Matrix.translation(-cx, -cy);
const scaling = Matrix.scaling(sx, sy);
const translateBack = Matrix.translation(cx, cy);
// 组合矩阵:translateBack × scaling × translateToOrigin
const combined = translateBack.multiply(scaling).multiply(translateToOrigin);
this._localMatrix = combined.multiply(this._localMatrix);
}
this.updateGlobalTransform();
this.queueRender();
return this;
}
/**
* 移动节点到指定的局部变换坐标
* @param x - 将节点移动到的目标 x 坐标
* @param y - 将节点移动到的目标 Y 坐标
* @returns 用于方法链的节点实例
*/
moveTo(x, y) {
const { x: currentX, y: currentY } = this.position;
this.move(x - currentX, y - currentY);
return this;
}
/**
* 将节点旋转到指定的局部变换角度
* @param angle - 目标旋转角度(以弧度为单位)
* @param cx - 旋转中心的 x 坐标(默认值:0)
* @param cy - 旋转中心的 Y 坐标(默认值:0)
* @returns 用于方法链的节点实例
*/
rotateTo(angle, cx = 0, cy = 0) {
const currentRotation = this.rotation;
this.rotate(angle - currentRotation, cx, cy);
return this;
}
/**
* 将节点缩放到指定的局部变换比例
* @param sx - 目标 X 轴缩放比例
* @param sy - 目标 Y 轴缩放比例
* @param cx - 缩放中心的 x 坐标(默认值:0)
* @param cy - 缩放中心的 Y 坐标(默认值:0)
* @returns 用于方法链的节点实例
*/
scaleTo(sx, sy, cx = 0, cy = 0) {
const { x: currentScaleX, y: currentScaleY } = this.scaling;
this.scale(sx / currentScaleX, sy / currentScaleY, cx, cy);
return this;
}
/**
* 将节点移动到指定的全局变换坐标
* @param x - 目标全局 x 坐标
* @param y - 目标全局 Y 坐标
* @returns 用于方法链的节点实例
*/
moveToGlobal(x, y) {
const { x: currentX, y: currentY } = this.globalPosition;
this.move(x - currentX, y - currentY);
return this;
}
/**
* 将节点旋转到指定的全局变换角度
* @param angle - 目标全局旋转角度(以弧度为单位)
* @param cx - 旋转中心的 x 坐标(默认值:0)
* @param cy - 旋转中心的 Y 坐标(默认值:0)
* @returns 用于方法链的节点实例
*/
rotateToGlobal(angle, cx = 0, cy = 0) {
const currentRotation = this.globalRotation;
this.rotate(angle - currentRotation, cx, cy);
return this;
}
/**
* 将节点缩放到指定的全局变换比例
* @param sx - 目标全局 X 轴缩放比例
* @param sy - 目标全局 Y 轴缩放比例
* @param cx - 缩放中心的 x 坐标(默认值:0)
* @param cy - 缩放中心的 Y 坐标(默认值:0)
* @returns 用于方法链的节点实例
*/
scaleToGlobal(sx, sy, cx = 0, cy = 0) {
const { x: currentScaleX, y: currentScaleY } = this.globalScaling;
this.scale(sx / currentScaleX, sy / currentScaleY, cx, cy);
return this;
}
/**
* 设置基础缩放因子
* @param factor 新的基础缩放因子
* @returns 当前节点实例,用于链式调用
*/
setZoomFactorBase(factor) {
this._zoomFactorBase = factor;
this._applyCurrentZoom(); // 可选:立即应用新的因子到当前级别
return this;
}
/**
* 放大节点
* @param cx - 缩放中心的 x 坐标(默认值:0)
* @param cy - 缩放中心的 y 坐标(默认值:0)
* @returns 当前节点实例,用于链式调用
*/
zoomIn(cx = 0, cy = 0) {
this._zoomLevel++;
this._applyCurrentZoom(cx, cy);
return this;
}
/**
* 缩小节点
* @param cx - 缩放中心的 x 坐标(默认值:0)
* @param cy - 缩放中心的 y 坐标(默认值:0)
* @returns 当前节点实例,用于链式调用
*/
zoomOut(cx = 0, cy = 0) {
this._zoomLevel--;
this._applyCurrentZoom(cx, cy);
return this;
}
/**
* 重置节点的缩放级别到初始状态 (级别0,缩放1)
* @param cx - 缩放中心的 x 坐标(默认值:0)
* @param cy - 缩放中心的 y 坐标(默认值:0)
* @returns 当前节点实例,用于链式调用
*/
resetZoom(cx = 0, cy = 0) {
this._zoomLevel = 0;
this._applyCurrentZoom(cx, cy);
return this;
}
/**
* 根据当前的缩放级别应用缩放
* @param cx - 缩放中心的 x 坐标(默认值:0)
* @param cy - 缩放中心的 y 坐标(默认值:0)
* @remarks
* 此方法会调用 scaleTo 来设置节点的缩放,确保精确性
*/
_applyCurrentZoom(cx = 0, cy = 0) {
const targetScale = Math.pow(this._zoomFactorBase, this._zoomLevel);
// 调用自身的 scaleTo 方法,该方法应处理局部缩放
this.scaleTo(targetScale, targetScale, cx, cy);
}
}
/**
* 抽象组类,表示可以包含其他节点的容器
*/
class Group extends Node {
/** 容器内包含的节点 */
children = [];
// 是否允许溢出/是否隐藏溢出内容
_overflowable = false;
/**
* Node 构造函数
* @param options 可选的构造参数对象
*/
constructor(options) {
const { id, zoomFactorBase } = options || {};
super({ id, zoomFactorBase });
}
/** 获取 是否允许溢出/是否隐藏溢出内容 */
get overflowable() { return this._overflowable; }
/** 设置 是否允许溢出/是否隐藏溢出内容 */
set overflowable(value) { this._overflowable = value; }
/** 设置父节点,同时更新所有子节点的层级 */
set parent(value) {
super.parent = value; // 调用基类的 setter,更新自身层级
this.updateChildrenLevels(); // 更新所有子节点层级
}
/** 获取父节点 */
get parent() {
return super.parent;
}
/**
* 获取子节点集合
* @returns 子节点数组的副本
*/
getChildren() {
return [...this.children];
}
/**
* 检查当前组是否包含指定的子节点
* @param node 要检查的节点
* @returns 如果节点是当前组的直接子节点,则返回 true,否则返回 false
*/
contains(node) {
return this.children.includes(node);
}
/**
* 添加子节点
* @param child 要添加的节点
* @returns 当前实例,用于链式调用
*/
addChild(child) {
this.children.push(child);
child.parent = this; // 设置父节点引用,这会触发 updateLevel
child.updateGlobalTransform(); // 更新子节点的全局变换
return this;
}
/**
* 移除子节点
* @param child 要移除的节点
* @returns 当前实例,用于链式调用
*/
removeChild(child) {
const index = this.children.indexOf(child);
if (index >= 0) {
this.children.splice(index, 1);
child.parent = undefined; // 移除父节点引用
}
return this;
}
/**
* 计算组内所有子节点的包围盒
* @returns 包含所有子节点的最小包围盒,如果没有子节点则返回一个原点处的零尺寸包围盒
*/
getBoundingBox() {
if (this.children.length === 0) {
// 对于空 Group,返回一个在原点,宽高为0的包围盒
// 或者可以根据 Group 自身是否有尺寸来决定,但目前 Group 没有直接的宽高属性
const ownGlobalPos = this.globalPosition;
return {
width: 0, height: 0,
x: ownGlobalPos.x, y: ownGlobalPos.y,
left: ownGlobalPos.x, top: ownGlobalPos.y,
right: ownGlobalPos.x, bottom: ownGlobalPos.y
};
}
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
// 遍历所有子节点,计算全局坐标系下的边界
for (const child of this.children) {
// 定义四个角点(子节点的局部坐标系)
const localCorners = [
{ x: 0, y: 0 },
{ x: child.width, y: 0 },
{ x: 0, y: child.height },
{ x: child.width, y: child.height } // 右下角
];
// 将角点转换为全局坐标系
for (const localCorner of localCorners) {
// 应用子节点的全局变换矩阵
const globalPoint = child.globalMatrix.transformPoint(localCorner.x, localCorner.y);
// 更新包围盒边界
minX = Math.min(minX, globalPoint.x);
minY = Math.min(minY, globalPoint.y);
maxX = Math.max(maxX, globalPoint.x);
maxY = Math.max(maxY, globalPoint.y);
}
// 如果子节点是 Group,递归获取其包围盒并合并
// 注意:如果一个 Group 本身没有子节点,其 getBoundingBox 会返回其自身位置的零尺寸包围盒
// 这意味着不需要特别处理 childBoundingBox 为 null 的情况,因为它总会返回一个 IBoundingBox
if (child instanceof Group) {
const childBoundingBox = child.getBoundingBox();
// 只有当子组的包围盒有效(即宽度或高度不为0,或者它不是初始的Infinity值)时才合并
// 或者更简单地,总是合并,因为Infinity会被任何实际数值覆盖
minX = Math.min(minX, childBoundingBox.left);
minY = Math.min(minY, childBoundingBox.top);
maxX = Math.max(maxX, childBoundingBox.right);
maxY = Math.max(maxY, childBoundingBox.bottom);
}
}
// 如果所有子节点都是零尺寸且位于同一位置,或者没有有效子节点进行计算(例如,初始值未被覆盖)
// 此时 minX, minY 可能仍然是 Infinity, maxX, maxY 是 -Infinity
// 这种情况下,返回一个基于组自身位置的零尺寸包围盒
if (minX === Infinity || minY === Infinity || maxX === -Infinity || maxY === -Infinity) {
const ownGlobalPos = this.globalPosition;
return {
width: 0, height: 0,
x: ownGlobalPos.x, y: ownGlobalPos.y,
left: ownGlobalPos.x, top: ownGlobalPos.y,
right: ownGlobalPos.x, bottom: ownGlobalPos.y
};
}
// 创建并返回包围盒
const boundingBox = {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
left: minX,
top: minY,
right: maxX,
bottom: maxY
};
return boundingBox;
}
/**
* 计算组内所有子节点在局部坐标系中的包围盒
* @returns 包含所有子节点的最小包围盒,如果没有子节点则返回一个原点处的零尺寸包围盒
*/
getLocalBoundingBox() {
if (this.children.length === 0) {
// 对于空 Group,返回一个在原点,宽高为0的包围盒
return {
width: 0, height: 0,
x: 0, y: 0,
left: 0, top: 0,
right: 0, bottom: 0
};
}
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
// 遍历所有子节点,计算局部坐标系下的边界
for (const child of this.children) {
// 定义四个角点(子节点的局部坐标系)
const localCorners = [
{ x: 0, y: 0 },
{ x: child.width, y: 0 },
{ x: 0, y: child.height },
{ x: child.width, y: child.height } // 右下角
];
// 将角点转换为当前 Group 的局部坐标系
for (const corner of localCorners) {
// 应用子节点的局部变换矩阵
const transformedPoint = child.localMatrix.transformPoint(corner.x, corner.y);
// 更新包围盒边界
minX = Math.min(minX, transformedPoint.x);
minY = Math.min(minY, transformedPoint.y);
maxX = Math.max(maxX, transformedPoint.x);
maxY = Math.max(maxY, transformedPoint.y);
}
// 如果子节点是 Group,递归获取其局部包围盒并合并
if (child instanceof Group) {
const childLocalBoundingBox = child.getLocalBoundingBox();
// 将子 Group 的局部包围盒转换到当前 Group 的局部坐标系
const corners = [
{ x: childLocalBoundingBox.left, y: childLocalBoundingBox.top },
{ x: childLocalBoundingBox.right, y: childLocalBoundingBox.top },
{ x: childLocalBoundingBox.left, y: childLocalBoundingBox.bottom },
{ x: childLocalBoundingBox.right, y: childLocalBoundingBox.bottom } // 右下角
];
for (const corner of corners) {
// 应用子节点的局部变换矩阵
const transformedPoint = child.localMatrix.transformPoint(corner.x, corner.y);
// 更新包围盒边界
minX = Math.min(minX, transformedPoint.x);
minY = Math.min(minY, transformedPoint.y);
maxX = Math.max(maxX, transformedPoint.x);
maxY = Math.max(maxY, transformedPoint.y);
}
}
}
// 处理边界情况:如果没有有效子节点或所有子节点都是零尺寸
if (minX === Infinity || minY === Infinity || maxX === -Infinity || maxY === -Infinity) {
return {
width: 0, height: 0,
x: 0, y: 0,
left: 0, top: 0,
right: 0, bottom: 0
};
}
// 创建并返回包围盒
const boundingBox = {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
left: minX,
top: minY,
right: maxX,
bottom: maxY
};
return boundingBox;
}
/** 更新自身及所有子元素的全局变换 */
updateGlobalTransform() {
super.updateGlobalTransform(); // 更新自身全局变换
// 更新所有子元素的全局变换
for (const child of this.children) {
child.updateGlobalTransform();
}
}
/**
* 应用字体样式
* @param fontStyle - 要应用的字体样式对象
*/
applyFont(fontStyle) {
super.applyFont(fontStyle);
this.target.applyFont(fontStyle);
}
/**
* 应用颜色样式
* @param colorStyle - 要应用的颜色样式对象
*/
applyColor(colorStyle) {
super.applyColor(colorStyle);
this.target.applyColor(colorStyle);
}
/**
* 应用边框样式
* @param borderStyle - 要应用的边框样式对象
*/
applyBorder(borderStyle) {
super.applyBorder(borderStyle);
this.target.applyBorder(borderStyle);
}
/**
* 应用背景样式
* @param backgroundStyle - 要应用的背景样式对象
*/
applyBackground(backgroundColor) {
super.applyBackground(backgroundColor);
this.target.applyBackground(backgroundColor);
}
// 更新所有子节点层级的方法(递归)
updateChildrenLevels() {
for (const child of this.children) {
child.updateLevel();
// 如果子节点是 Group,递归更新其子节点
if (child instanceof Group) {
child.updateChildrenLevels();
}
}
}
}
/**
* 定义了一个 DOM 元素的包装器类,提供了一些方法来操作和管理 DOM 元素的样式、位置、旋转、缩放等属性
* 其中关于 DOM 元素的定位,是基于父元素是已定位(非 static),子元素是绝对定位的原则
* 在 CSS 布局中,当涉及到绝对定位的子元素与已定位父元素的关系时,定位参考基准如下(由浏览器决定的):
* 父元素:绝对定位子元素的参考点是父元素的 padding box(即 border 的内边缘)
* 子元素:子元素的 margin 外边缘是被定位的部分
*/
class DomWrapper {
_htmlEle;
_node;
_rotation = 0; // 存储当前旋转角度(弧度)
_scaleX = 1; // 存储当前X轴缩放比例
_scaleY = 1; // 存储当前Y轴缩放比例
get htmlEle() { return this._htmlEle; }
get node() { return this._node; }
constructor(node, dom) {
this._htmlEle = dom;
this._node = node;
// 同步 Node ID 到 DOM Element ID
if (this._node.id) {
// 如果 DOM 元素还没有 ID,或者 ID 与 Node 的 ID 不同,则设置它
if (!this._htmlEle.id || this._htmlEle.id !== this._node.id) {
this._htmlEle.id = this._node.id;
}
}
this.htmlEle.style.transformOrigin = '0 0'; // 设置变换原点为左上角
// 应用基本样式(不依赖parent)
this.applyBaseStyles();
this.setLevel(node.level); // 设置层级
}
// 应用基本样式
applyBaseStyles() {
// 所有元素都应用的基本样式
this.htmlEle.style.boxSizing = 'border-box';
this.htmlEle.style.margin = '0';
if (this._node instanceof Group) {
this.setOverflow(this._node.overflowable); // 设置溢出属性
// 去掉右键菜单
this.htmlEle.addEventListener('contextmenu', (event) => {
event.preventDefault();
return false;
});
}
else {
// 普通元素默认设为绝对定位
this.htmlEle.style.position = 'absolute';
}
}
// 更新定位样式
updatePositioningStyle() {
if (this._node.parent) {
// 有父节点时使用绝对定位
this.htmlEle.style.position = 'absolute';
if (this._node instanceof Group) {
this.htmlEle.style.border = 'none'; // 去除嵌套组边框
}
}
else {
// 是根节点时,如果没有设置定位,则设置为相对定位
const computedStyle = window.getComputedStyle(this.htmlEle);
if (computedStyle.position === 'static') {
this.htmlEle.style.position = 'relative';
}
}
}
// 更新 DOM 元素的 transform 属性
_updateTransform() {
let transform = '';
// 按照固定顺序应用变换(先旋转后缩放)
if (this._rotation !== 0) {
transform += `rotate(${this._rotation}rad) `;
}
if (this._scaleX !== 1 || this._scaleY !== 1) {
transform += `scale(${this._scaleX}, ${this._scaleY})`;
}
this._htmlEle.style.transform = transform.trim();
}
// 实现 IDomWrapable 接口方法
addEventListener(event, handler, options) {
this._htmlEle.addEventListener(event, handler, options);
}
// 实现 IDomWrapable 接口方法
removeEventListener(event, handler) {
this._htmlEle.removeEventListener(event, handler);
}
// 设置层级
setLevel(level) {
if (level > 0) {
this._htmlEle.style.zIndex = level.toString();
}
}
setOverflow(overflowable) {
this._htmlEle.style.overflow = overflowable ? 'visible' : 'hidden';
}
moveTo(x, y) {
this._htmlEle.style.left = x + "px";
this._htmlEle.style.top = y + "px";
}
rotateTo(angle) {
this._rotation = angle;
this._updateTransform();
}
scaleTo(sx, sy) {
this._scaleX = sx;
this._scaleY = sy;
this._updateTransform();
}
setWidth(width) {
this.setSize({ width });
}
setHeight(height) {
this.setSize({ height });
}
setSize(size) {
const { width, height } = size;
if (width !== undefined)
this._htmlEle.style.width = width + 'px';
if (height !== undefined)
this._htmlEle.style.height = height + 'px';
}
get offsetWidth() {
return this._htmlEle.offsetWidth;
}
get offsetHeight() {
return this._htmlEle.offsetHeight;
}
getBoundingClientRect() {
return this._htmlEle.getBoundingClientRect();
}
applyFont(font) {
const { fontFamily, fontSize, fontStyle, fontWeight } = font;
if (fontFamily) {
this._htmlEle.style.fontFamily = fontFamily;
}
if (fontSize) {
this._htmlEle.style.fontSize = fontSize + 'px';
}
if (fontStyle) {
this._htmlEle.style.fontStyle = fontStyle;
}
if (fontWeight) {
this._htmlEle.style.fontWeight = fontWeight;
}
}
applyColor(colorStyle) {
if (colorStyle.color) {
this._htmlEle.style.color = colorStyle.color;
}
}
applyBorder(border) {
const { borderWidth, borderColor, borderStyle } = border;
if (borderWidth) {
this._htmlEle.style.borderWidth = borderWidth + 'px';
}
if (borderColor) {
this._htmlEle.style.borderColor = borderColor;
}
if (borderStyle) {
this._htmlEle.style.borderStyle = borderStyle;
}
}
applyBackground(backgroundStyle) {
const { backgroundColor, opacity } = backgroundStyle;
if (backgroundColor) {
this._htmlEle.style.backgroundColor = backgroundColor;
}
if (opacity !== undefined) {
this._htmlEle.style.opacity = opacity.toString();
}
}
}
const DEFAULT_WIDTH = 100;
const DEFAULT_HEIGHT = 100;
var ElementEvent;
(function (ElementEvent) {
ElementEvent["DISPOSE"] = "dispose";
})(ElementEvent || (ElementEvent = {}));
/**
* Element 类是 DOM 元素的封装类,继承自 Node
* 提供对 HTML 元素的操作、样式调整和 DOM 树操作等功能
* 作为 2D 可视化系统中处理实际 DOM 元素渲染的基础类
*/
class Element extends Node {
// 标记元素是否已经添加到 DOM 中
isAppended = false;
// 元素宽度(不会因元素的内容而改变,原则上内容宽度不可超出本值)
_width = 0;
// 元素高度(不会因元素的内容而改变,原则上内容高度不可超出本值)
_height = 0;
/** 目标包装器,负责实际操作 DOM 元素 */
target;
/** 获取元素宽度 */
get width() { return this._width; }
/** 获取元素高度 */
get height() { return this._height; }
/**
* 创建一个元素实例
* @param options 配置对象,可指定 DOM 元素、宽度、高度和 ID
*/
constructor(options) {
const { id, dom } = options || {};
let initialId = undefined;
let elementDom;
// 确定 ID:优先使用 options.id,其次使用 dom.id
if (id) {
initialId = id;
}
else if (dom?.id) {
initialId = dom.id;
}
super({ id: initialId }); // 调用 Node 构造函数,传递 id
// 判断是否提供了 DOM 元素
if (dom) {
// 使用现有的 DOM 元素
elementDom = dom;
this.target = new DomWrapper(this, elementDom);
this._width = elementDom.offsetWidth;
this._height = elementDom.offsetHeight;
}
else {
// 创建新的 div 元素
elementDom = document.createElement('div');
this.target = new DomWrapper(this, elementDom);
// 使用配置对象或默认值设置尺寸
const { width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = options || {};
this.setWidth(width);
this.setHeight(height);
}
}
/**
* 更新目标元素的变换(位置、旋转、缩放等)
*/
updateTargetTransform() {
// const { x, y } = this.globalPosition;
// const rotation = this.globalRotation;
// const { x: scaleX, y: scaleY } = this.globalScaling;
const { x, y } = this.position;
const rotation = this.rotation;
const { x: scaleX, y: scaleY } = this.scaling;
this.target?.moveTo(x, y);
this.target?.rotateTo(rotation);
this.target?.scaleTo(scaleX, scaleY);
}
/**
* 在元素附加到 DOM 前更新定位样式
*/
updateDomPositioningStyle() {
if (this.target && this.target instanceof DomWrapper) {
this.target.updatePositioningStyle();
}
}
/**
* 实现渲染,将元素附加到 DOM 并更新变换
*/
render() {
this.attachToDOM();
this.updateTargetTransform();
}
/**
* 设置元素宽度
* @param value 要设置的宽度值
*/
setWidth(value) {
this.setSize({ width: value });
}
/**
* 设置元素高度
* @param value 要设置的高度值
*/
setHeight(value) {
this.setSize({ height: value });
}
/**
* 设置元素尺寸
* @param size 包含宽度和/或高度的对象
*/
setSize(size) {
const { width, height } = size;
if (width !== undefined) {
this._width = width;
this.target.setWidth(width);
}
if (height !== undefined) {
this._height = height;