{T}

事件系统

Three.js 的事件系统包括渲染器 DOM 事件、场景事件和自定义事件,通过事件驱动实现复杂的交互逻辑。

概述

Three.js 提供了完整的事件系统,支持鼠标、键盘、触摸、窗口等各种事件,以及基于 EventDispatcher 的自定义事件机制。本文档详细介绍各类事件的使用方法和最佳实践。

事件系统架构

plaintext
┌─────────────────────────────────────────────────────────────┐
│                      Three.js 事件系统                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐ │
│  │   DOM 事件       │  │  Object3D 事件  │  │  自定义事件  │ │
│  ├─────────────────┤  ├─────────────────┤  ├─────────────┤ │
│  │ • mousemove     │  │ • added         │  │ • EventDisp │ │
│  │ • mousedown/up  │  │ • removed       │  │   atcher    │ │
│  │ • click         │  │ • change        │  │ • 自定义类型 │ │
│  │ • wheel         │  │ • dispose       │  │             │ │
│  │ • keydown/up    │  │                 │  │             │ │
│  │ • touchstart    │  │                 │  │             │ │
│  │ • touchmove     │  │                 │  │             │ │
│  │ • resize        │  │                 │  │             │ │
│  └─────────────────┘  └─────────────────┘  └─────────────┘ │
│           │                   │                   │        │
│           └───────────────────┴───────────────────┘        │
│                              │                              │
│                    ┌─────────▼─────────┐                   │
│                    │   事件管理器/分发器  │                   │
│                    └───────────────────┘                   │
│                              │                              │
│                    ┌─────────▼─────────┐                   │
│                    │   回调函数/处理逻辑  │                   │
│                    └───────────────────┘                   │
└─────────────────────────────────────────────────────────────┘

事件类型对比

类型来源用途示例
DOM 事件浏览器用户输入处理鼠标点击、键盘按键
Object3D 事件Three.js 内部对象状态变化添加到场景、移除
自定义事件开发者定义业务逻辑通信对象选中、动画完成

渲染器 DOM 事件

鼠标事件

javascript
import * as THREE from 'three';
 
const renderer = new THREE.WebGLRenderer();
document.body.appendChild(renderer.domElement);
 
// 鼠标按下
renderer.domElement.addEventListener('mousedown', (event) => {
  console.log('鼠标按下:', event.button);
  // event.button: 0=左键, 1=中键, 2=右键
});
 
// 鼠标释放
renderer.domElement.addEventListener('mouseup', (event) => {
  console.log('鼠标释放:', event.button);
});
 
// 鼠标移动
renderer.domElement.addEventListener('mousemove', (event) => {
  console.log('鼠标移动:', event.clientX, event.clientY);
});
 
// 鼠标点击(完整点击序列:mousedown → mouseup → click)
renderer.domElement.addEventListener('click', (event) => {
  console.log('鼠标点击');
});
 
// 双击
renderer.domElement.addEventListener('dblclick', (event) => {
  console.log('双击');
});
 
// 鼠标进入画布
renderer.domElement.addEventListener('mouseenter', (event) => {
  console.log('鼠标进入');
});
 
// 鼠标离开画布
renderer.domElement.addEventListener('mouseleave', (event) => {
  console.log('鼠标离开');
});
 
// 滚轮
renderer.domElement.addEventListener('wheel', (event) => {
  console.log('滚轮:', event.deltaY);
  event.preventDefault();  // 阻止页面滚动
});
 
// 上下文菜单(右键菜单)
renderer.domElement.addEventListener('contextmenu', (event) => {
  event.preventDefault();  // 阻止默认菜单
  console.log('右键菜单');
});

鼠标事件对象属性

属性类型说明
clientX/Ynumber相对于视口的坐标
pageX/Ynumber相对于页面的坐标(含滚动)
offsetX/Ynumber相对于目标元素的坐标
movementX/Ynumber相对于上一次移动的偏移
buttonnumber按下的按钮(0=左, 1=中, 2=右)
buttonsnumber当前按下的按钮组合(位掩码)
shiftKeybooleanShift 是否按下
ctrlKeybooleanCtrl 是否按下
altKeybooleanAlt 是否按下
metaKeybooleanMeta/Command 是否按下

键盘事件

javascript
// 键盘按下
window.addEventListener('keydown', (event) => {
  console.log('按键:', event.key, event.code, event.keyCode);
  
  // 常用按键检测
  if (event.key === 'w' || event.key === 'W') {
    console.log('按下 W 键');
  }
  
  if (event.key === 'ArrowUp') {
    console.log('按下上箭头');
  }
  
  // 组合键
  if (event.ctrlKey && event.key === 's') {
    event.preventDefault();
    console.log('Ctrl+S');
  }
  
  if (event.shiftKey) {
    console.log('按住 Shift');
  }
  
  if (event.altKey && event.key === 'Enter') {
    console.log('Alt+Enter');
  }
});
 
// 键盘释放
window.addEventListener('keyup', (event) => {
  console.log('释放:', event.key);
});

按键持续检测

javascript
// 使用按键状态对象
const keys = {};
 
window.addEventListener('keydown', (event) => {
  keys[event.code] = true;
});
 
window.addEventListener('keyup', (event) => {
  keys[event.code] = false;
});
 
// 在动画循环中检测
function animate() {
  requestAnimationFrame(animate);
  
  const speed = 0.1;
  
  if (keys['KeyW'] || keys['ArrowUp']) {
    camera.position.z -= speed;
  }
  if (keys['KeyS'] || keys['ArrowDown']) {
    camera.position.z += speed;
  }
  if (keys['KeyA'] || keys['ArrowLeft']) {
    camera.position.x -= speed;
  }
  if (keys['KeyD'] || keys['ArrowRight']) {
    camera.position.x += speed;
  }
  if (keys['Space']) {
    camera.position.y += speed;
  }
  if (keys['ShiftLeft'] || keys['ShiftRight']) {
    camera.position.y -= speed;
  }
  
  renderer.render(scene, camera);
}
animate();

键盘事件属性

属性类型说明
keystring按键值(如 'a', 'Enter', 'ArrowUp')
codestring物理按键代码(如 'KeyA', 'Enter', 'ArrowUp')
keyCodenumber已废弃,建议使用 key 或 code
repeatboolean是否为重复按键(长按)

触摸事件

javascript
// 触摸开始
renderer.domElement.addEventListener('touchstart', (event) => {
  event.preventDefault();  // 阻止默认行为(如滚动)
  
  const touches = event.touches;
  console.log(`触摸开始: ${touches.length} 个触摸点`);
  
  for (let i = 0; i < touches.length; i++) {
    console.log(`触摸点 ${i}:`, touches[i].clientX, touches[i].clientY);
  }
});
 
// 触摸移动
renderer.domElement.addEventListener('touchmove', (event) => {
  event.preventDefault();  // 阻止页面滚动
  
  const touches = event.touches;
  console.log('触摸移动:', touches.length, '个触摸点');
});
 
// 触摸结束
renderer.domElement.addEventListener('touchend', (event) => {
  console.log('触摸结束');
  console.log('剩余触摸点:', event.touches.length);
});
 
// 触摸取消(来电、系统弹窗等)
renderer.domElement.addEventListener('touchcancel', (event) => {
  console.log('触摸取消');
});

触摸事件对象

javascript
renderer.domElement.addEventListener('touchstart', (event) => {
  // touches: 当前屏幕上的所有触摸点
  const touches = event.touches;
  
  // targetTouches: 当前元素上的所有触摸点
  const targetTouches = event.targetTouches;
  
  // changedTouches: 触发此次事件的触摸点
  const changedTouches = event.changedTouches;
  
  // 单个触摸点信息
  const touch = touches[0];
  console.log({
    identifier: touch.identifier,  // 唯一标识
    clientX: touch.clientX,        // 视口 X 坐标
    clientY: touch.clientY,        // 视口 Y 坐标
    pageX: touch.pageX,            // 页面 X 坐标
    pageY: touch.pageY,            // 页面 Y 坐标
    screenX: touch.screenX,        // 屏幕 X 坐标
    screenY: touch.screenY         // 屏幕 Y 坐标
  });
});

多点触控示例

javascript
// 缩放示例(双指缩放)
let lastTouchDistance = 0;
 
renderer.domElement.addEventListener('touchmove', (event) => {
  if (event.touches.length === 2) {
    // 计算两个触摸点之间的距离
    const dx = event.touches[0].clientX - event.touches[1].clientX;
    const dy = event.touches[0].clientY - event.touches[1].clientY;
    const distance = Math.sqrt(dx * dx + dy * dy);
    
    if (lastTouchDistance > 0) {
      // 计算缩放比例
      const scale = distance / lastTouchDistance;
      
      // 应用缩放(示例:缩放相机距离)
      camera.position.multiplyScalar(1 / scale);
    }
    
    lastTouchDistance = distance;
  }
});
 
renderer.domElement.addEventListener('touchend', () => {
  lastTouchDistance = 0;
});
 
// 旋转示例(双指旋转)
let lastAngle = 0;
 
renderer.domElement.addEventListener('touchmove', (event) => {
  if (event.touches.length === 2) {
    const angle = Math.atan2(
      event.touches[1].clientY - event.touches[0].clientY,
      event.touches[1].clientX - event.touches[0].clientX
    );
    
    if (lastAngle !== 0) {
      const deltaAngle = angle - lastAngle;
      // 应用旋转
      scene.rotation.y += deltaAngle;
    }
    
    lastAngle = angle;
  }
});
 
renderer.domElement.addEventListener('touchend', () => {
  lastAngle = 0;
});
 
// 拖拽示例(单指拖拽)
let lastTouchX = 0;
let lastTouchY = 0;
 
renderer.domElement.addEventListener('touchstart', (event) => {
  if (event.touches.length === 1) {
    lastTouchX = event.touches[0].clientX;
    lastTouchY = event.touches[0].clientY;
  }
});
 
renderer.domElement.addEventListener('touchmove', (event) => {
  if (event.touches.length === 1) {
    const deltaX = event.touches[0].clientX - lastTouchX;
    const deltaY = event.touches[0].clientY - lastTouchY;
    
    // 应用移动
    camera.position.x -= deltaX * 0.01;
    camera.position.y += deltaY * 0.01;
    
    lastTouchX = event.touches[0].clientX;
    lastTouchY = event.touches[0].clientY;
  }
});

窗口事件

javascript
// 窗口大小改变
window.addEventListener('resize', (event) => {
  const width = window.innerWidth;
  const height = window.innerHeight;
  
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  
  renderer.setSize(width, height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
 
// 设备方向变化(移动端)
window.addEventListener('orientationchange', () => {
  console.log('屏幕方向:', screen.orientation.type);
});
 
// 窗口获得焦点
window.addEventListener('focus', () => {
  console.log('窗口获得焦点');
  // 可用于恢复动画
});
 
// 窗口失去焦点
window.addEventListener('blur', () => {
  console.log('窗口失去焦点');
  // 可用于暂停动画
});
 
// 页面可见性改变
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    console.log('页面隐藏');
    // 暂停动画、释放资源
  } else {
    console.log('页面显示');
    // 恢复动画
  }
});
 
// 页面卸载前
window.addEventListener('beforeunload', (event) => {
  // 保存状态、清理资源
  console.log('页面即将卸载');
});
 
// 设备像素比变化(高分屏切换)
window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
  .addEventListener('change', () => {
    renderer.setPixelRatio(window.devicePixelRatio);
  });

Object3D 事件

Three.js 的 Object3D 使用 EventDispatcher 提供事件系统。

内置事件

javascript
const mesh = new THREE.Mesh();
 
// 监听添加到场景事件
mesh.addEventListener('added', (event) => {
  console.log('对象添加到场景', event.target);
});
 
// 监听从场景移除事件
mesh.addEventListener('removed', (event) => {
  console.log('对象从场景移除', event.target);
});
 
// 添加到场景(触发 'added' 事件)
scene.add(mesh);
 
// 从场景移除(触发 'removed' 事件)
scene.remove(mesh);

自定义事件

javascript
const mesh = new THREE.Mesh();
 
// 监听自定义事件
mesh.addEventListener('selected', (event) => {
  console.log('对象被选中:', event.message);
  console.log('选中坐标:', event.point);
});
 
mesh.addEventListener('deselected', (event) => {
  console.log('对象取消选中');
});
 
// 派发自定义事件
function selectObject(mesh, point) {
  mesh.dispatchEvent({
    type: 'selected',
    message: 'User selected this object',
    point: point,
    timestamp: Date.now()
  });
}
 
// 取消选中
function deselectObject(mesh) {
  mesh.dispatchEvent({
    type: 'deselected'
  });
}
 
// 使用示例
selectObject(mesh, new THREE.Vector3(1, 2, 3));

完整的自定义事件系统

javascript
class InteractiveMesh extends THREE.Mesh {
  constructor(geometry, material) {
    super(geometry, material);
    
    this.userData = {
      isHovered: false,
      isSelected: false
    };
  }
  
  onHover(point) {
    if (!this.userData.isHovered) {
      this.userData.isHovered = true;
      this.dispatchEvent({
        type: 'hover',
        point: point
      });
    }
  }
  
  onLeave() {
    if (this.userData.isHovered) {
      this.userData.isHovered = false;
      this.dispatchEvent({
        type: 'leave'
      });
    }
  }
  
  onSelect(point) {
    if (!this.userData.isSelected) {
      this.userData.isSelected = true;
      this.dispatchEvent({
        type: 'select',
        point: point
      });
    }
  }
  
  onDeselect() {
    if (this.userData.isSelected) {
      this.userData.isSelected = false;
      this.dispatchEvent({
        type: 'deselect'
      });
    }
  }
}
 
// 使用
const interactiveMesh = new InteractiveMesh(geometry, material);
 
interactiveMesh.addEventListener('hover', (event) => {
  event.target.material.emissive.setHex(0x333333);
});
 
interactiveMesh.addEventListener('leave', (event) => {
  event.target.material.emissive.setHex(0x000000);
});
 
interactiveMesh.addEventListener('select', (event) => {
  event.target.material.color.setHex(0xff0000);
});
 
interactiveMesh.addEventListener('deselect', (event) => {
  event.target.material.color.setHex(0x00ff00);
});

事件工具类

事件管理器

javascript
class EventManager {
  constructor() {
    this.listeners = {};
  }
  
  // 添加事件监听
  on(event, callback, context) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    
    const listener = { callback, context };
    this.listeners[event].push(listener);
    
    // 返回取消监听函数
    return () => this.off(event, callback);
  }
  
  // 移除事件监听
  off(event, callback) {
    if (!this.listeners[event]) return;
    
    if (callback) {
      this.listeners[event] = this.listeners[event].filter(
        listener => listener.callback !== callback
      );
    } else {
      delete this.listeners[event];
    }
  }
  
  // 触发事件
  emit(event, ...args) {
    if (!this.listeners[event]) return;
    
    this.listeners[event].forEach(listener => {
      listener.callback.apply(listener.context, args);
    });
  }
  
  // 一次性监听
  once(event, callback, context) {
    const wrapper = (...args) => {
      callback.apply(context, args);
      this.off(event, wrapper);
    };
    
    return this.on(event, wrapper, context);
  }
  
  // 清除所有监听
  clear() {
    this.listeners = {};
  }
}
 
// 使用示例
const eventManager = new EventManager();
 
// 监听事件
const unsubscribe = eventManager.on('objectSelected', (object) => {
  console.log('选中对象:', object);
});
 
// 触发事件
eventManager.emit('objectSelected', mesh);
 
// 移除监听
unsubscribe();
// 或
eventManager.off('objectSelected');
 
// 一次性监听
eventManager.once('init', () => {
  console.log('只执行一次');
});

鼠标状态管理器

javascript
class MouseState {
  constructor(element) {
    this.element = element;
    
    // 鼠标位置
    this.position = new THREE.Vector2();
    this.normalizedPosition = new THREE.Vector2();
    this.delta = new THREE.Vector2();
    
    // 按钮状态 [左, 中, 右]
    this.buttons = [false, false, false];
    
    // 修饰键状态
    this.keys = {
      shift: false,
      ctrl: false,
      alt: false,
      meta: false
    };
    
    // 滚轮
    this.wheelDelta = 0;
    
    // 内部状态
    this.previousPosition = new THREE.Vector2();
    this.isFirstMove = true;
    
    this.setupListeners();
  }
  
  setupListeners() {
    this._onMouseDown = this.onMouseDown.bind(this);
    this._onMouseUp = this.onMouseUp.bind(this);
    this._onMouseMove = this.onMouseMove.bind(this);
    this._onWheel = this.onWheel.bind(this);
    this._onKeyDown = this.onKeyDown.bind(this);
    this._onKeyUp = this.onKeyUp.bind(this);
    this._onContextMenu = this.onContextMenu.bind(this);
    
    this.element.addEventListener('mousedown', this._onMouseDown);
    this.element.addEventListener('mouseup', this._onMouseUp);
    this.element.addEventListener('mousemove', this._onMouseMove);
    this.element.addEventListener('wheel', this._onWheel);
    this.element.addEventListener('contextmenu', this._onContextMenu);
    window.addEventListener('keydown', this._onKeyDown);
    window.addEventListener('keyup', this._onKeyUp);
  }
  
  dispose() {
    this.element.removeEventListener('mousedown', this._onMouseDown);
    this.element.removeEventListener('mouseup', this._onMouseUp);
    this.element.removeEventListener('mousemove', this._onMouseMove);
    this.element.removeEventListener('wheel', this._onWheel);
    this.element.removeEventListener('contextmenu', this._onContextMenu);
    window.removeEventListener('keydown', this._onKeyDown);
    window.removeEventListener('keyup', this._onKeyUp);
  }
  
  onMouseDown(event) {
    this.buttons[event.button] = true;
    this.updatePosition(event);
  }
  
  onMouseUp(event) {
    this.buttons[event.button] = false;
  }
  
  onMouseMove(event) {
    this.updatePosition(event);
  }
  
  onWheel(event) {
    this.wheelDelta = event.deltaY;
  }
  
  onKeyDown(event) {
    this.keys.shift = event.shiftKey;
    this.keys.ctrl = event.ctrlKey;
    this.keys.alt = event.altKey;
    this.keys.meta = event.metaKey;
  }
  
  onKeyUp(event) {
    this.keys.shift = event.shiftKey;
    this.keys.ctrl = event.ctrlKey;
    this.keys.alt = event.altKey;
    this.keys.meta = event.metaKey;
  }
  
  onContextMenu(event) {
    event.preventDefault();
  }
  
  updatePosition(event) {
    const rect = this.element.getBoundingClientRect();
    
    // 像素位置
    this.position.set(
      event.clientX - rect.left,
      event.clientY - rect.top
    );
    
    // NDC 坐标
    this.normalizedPosition.set(
      (event.clientX - rect.left) / rect.width * 2 - 1,
      -((event.clientY - rect.top) / rect.height) * 2 + 1
    );
    
    // 计算移动增量
    if (!this.isFirstMove) {
      this.delta.set(
        this.position.x - this.previousPosition.x,
        this.position.y - this.previousPosition.y
      );
    } else {
      this.delta.set(0, 0);
      this.isFirstMove = false;
    }
    
    this.previousPosition.copy(this.position);
  }
  
  // 便捷方法
  isLeftButtonPressed() { return this.buttons[0]; }
  isMiddleButtonPressed() { return this.buttons[1]; }
  isRightButtonPressed() { return this.buttons[2]; }
  isShiftPressed() { return this.keys.shift; }
  isCtrlPressed() { return this.keys.ctrl; }
}
 
// 使用
const mouseState = new MouseState(renderer.domElement);
 
function animate() {
  requestAnimationFrame(animate);
  
  if (mouseState.isLeftButtonPressed()) {
    // 按住左键时执行操作
    camera.rotation.y += mouseState.delta.x * 0.01;
  }
  
  renderer.render(scene, camera);
}

交互控制器

DragControls

用于拖拽场景中的对象。

javascript
import { DragControls } from 'three/addons/controls/DragControls.js';
 
const controls = new DragControls(objects, camera, renderer.domElement);
 
// 拖拽开始
controls.addEventListener('dragstart', (event) => {
  console.log('开始拖拽:', event.object);
  event.object.material.emissive.setHex(0x444444);
  
  // 禁用轨道控制器
  orbitControls.enabled = false;
});
 
// 拖拽中
controls.addEventListener('drag', (event) => {
  console.log('拖拽位置:', event.object.position);
  // 可以在这里添加约束逻辑
});
 
// 拖拽结束
controls.addEventListener('dragend', (event) => {
  console.log('结束拖拽');
  event.object.material.emissive.setHex(0x000000);
  
  orbitControls.enabled = true;
});
 
// 鼠标悬停
controls.addEventListener('hoveron', (event) => {
  console.log('悬停:', event.object);
  document.body.style.cursor = 'grab';
});
 
controls.addEventListener('hoveroff', (event) => {
  document.body.style.cursor = 'default';
});
 
// 销毁
controls.dispose();

TransformControls

用于移动、旋转、缩放对象的可视化控制器。

javascript
import { TransformControls } from 'three/addons/controls/TransformControls.js';
 
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
 
// 选中对象
transformControls.attach(mesh);
 
// 变换模式
transformControls.setMode('translate');  // 移动
transformControls.setMode('rotate');     // 旋转
transformControls.setMode('scale');      // 缩放
 
// 设置控制器大小
transformControls.setSize(1);
 
// 监听变换事件
transformControls.addEventListener('dragging-changed', (event) => {
  // 拖拽时禁用轨道控制器
  orbitControls.enabled = !event.value;
});
 
transformControls.addEventListener('change', () => {
  console.log('对象变换:', transformControls.object.position);
});
 
transformControls.addEventListener('objectChange', () => {
  // 对象位置/旋转/缩放改变
});
 
// 键盘切换模式
window.addEventListener('keydown', (event) => {
  switch (event.key.toLowerCase()) {
    case 'w':
      transformControls.setMode('translate');
      break;
    case 'e':
      transformControls.setMode('rotate');
      break;
    case 'r':
      transformControls.setMode('scale');
      break;
    case 'escape':
      transformControls.detach();
      break;
  }
});
 
// 设置变换空间
transformControls.setSpace('world');  // 世界空间
transformControls.setSpace('local');  // 局部空间
 
// 启用/禁用特定轴
transformControls.showX = true;
transformControls.showY = true;
transformControls.showZ = false;  // 禁止 Z 轴变换

OrbitControls 与其他控制器的配合

javascript
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
 
const orbitControls = new OrbitControls(camera, renderer.domElement);
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
 
// 关键:当使用 TransformControls 时禁用 OrbitControls
transformControls.addEventListener('dragging-changed', (event) => {
  orbitControls.enabled = !event.value;
});
 
// 点击选择对象
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
 
renderer.domElement.addEventListener('click', (event) => {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(objects);
  
  if (intersects.length > 0) {
    transformControls.attach(intersects[0].object);
  } else {
    transformControls.detach();
  }
});

API 参考

EventDispatcher 类

所有 Three.js 对象的事件基类。

javascript
// 创建可派发事件的对象
const dispatcher = new THREE.EventDispatcher();
 
// 添加监听器
dispatcher.addEventListener('event', callback);
 
// 移除监听器
dispatcher.removeEventListener('event', callback);
 
// 检查是否有监听器
dispatcher.hasEventListener('event', callback);
 
// 派发事件
dispatcher.dispatchEvent({ type: 'event', data: {} });

EventDispatcher 方法

方法参数说明
addEventListener(type, listener)string, Function添加事件监听器
hasEventListener(type, listener)string, Function检查是否有监听器
removeEventListener(type, listener)string, Function移除事件监听器
dispatchEvent(event)Object派发事件对象

DragControls 事件

事件名说明event 属性
dragstart开始拖拽object
drag拖拽中object
dragend结束拖拽object
hoveron鼠标悬停object
hoveroff鼠标离开object

TransformControls 事件

事件名说明event 属性
dragging-changed拖拽状态变化value (boolean)
change控制器变化-
objectChange对象变换-
mouseUp鼠标释放-

TransformControls 属性

属性类型说明
modestring当前模式:'translate', 'rotate', 'scale'
objectObject3D当前附加的对象
showXboolean显示 X 轴控制器
showYboolean显示 Y 轴控制器
showZboolean显示 Z 轴控制器
sizenumber控制器大小
spacestring变换空间:'world' 或 'local'

常见问题

Q: 事件监听器内存泄漏?

A: 确保在不需要时移除监听器:

javascript
// 错误做法
window.addEventListener('resize', handleResize);
// 没有移除监听器
 
// 正确做法
class MyComponent {
  constructor() {
    this.handleResize = this.handleResize.bind(this);
    window.addEventListener('resize', this.handleResize);
  }
  
  dispose() {
    window.removeEventListener('resize', this.handleResize);
  }
  
  handleResize() {
    // ...
  }
}

Q: 触摸事件和鼠标事件冲突?

A: 移动设备上触摸事件会触发模拟的鼠标事件:

javascript
// 防止触摸触发鼠标事件
renderer.domElement.addEventListener('touchstart', (event) => {
  event.preventDefault();
}, { passive: false });
 
// 或检测设备类型
const isTouchDevice = 'ontouchstart' in window;
if (isTouchDevice) {
  // 只绑定触摸事件
} else {
  // 只绑定鼠标事件
}

Q: 键盘事件不响应?

A: 键盘事件需要元素获得焦点:

javascript
// 确保画布可以获得焦点
renderer.domElement.tabIndex = 0;
renderer.domElement.focus();
 
// 或在 window 上监听
window.addEventListener('keydown', handler);

Q: 事件处理影响帧率?

A: 使用节流或防抖:

javascript
// 节流
function throttle(fn, delay) {
  let last = 0;
  return function(...args) {
    const now = Date.now();
    if (now - last >= delay) {
      last = now;
      fn.apply(this, args);
    }
  };
}
 
// 防抖
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
 
// 使用
renderer.domElement.addEventListener(
  'mousemove',
  throttle(handleMouseMove, 50)
);

Q: TransformControls 和 OrbitControls 冲突?

A: 在使用 TransformControls 时禁用 OrbitControls:

javascript
transformControls.addEventListener('dragging-changed', (event) => {
  orbitControls.enabled = !event.value;
});

Q: 移动端双指缩放灵敏度问题?

A: 调整缩放系数:

javascript
let lastTouchDistance = 0;
const zoomSpeed = 0.01;  // 调整这个值
 
renderer.domElement.addEventListener('touchmove', (event) => {
  if (event.touches.length === 2) {
    const distance = getTouchDistance(event.touches);
    
    if (lastTouchDistance > 0) {
      const delta = (distance - lastTouchDistance) * zoomSpeed;
      camera.fov = Math.max(20, Math.min(120, camera.fov - delta * 10));
      camera.updateProjectionMatrix();
    }
    
    lastTouchDistance = distance;
  }
});

最佳实践

  1. 及时清理监听器:组件销毁时移除事件监听器,防止内存泄漏
  2. 事件委托:对于多个相似元素,使用事件委托减少监听器数量
  3. 节流防抖:对频繁触发的事件使用节流或防抖优化性能
  4. 状态管理:使用状态对象管理按键、鼠标等状态
  5. 跨平台兼容:同时支持鼠标和触摸事件
  6. 可访问性:提供键盘操作支持
  7. 事件命名:自定义事件使用清晰、一致的命名规范

相关链接