故国神游,多情应笑我,早生华发。

在Three.js中实现类似 Ctrl+Z 的撤销功能,核心是通过手动记录场景的状态或操作历史,并在需要时回退到之前的状态。

1. 核心思路

  • 状态记录:每次对场景进行修改(如添加/删除物体、修改位置、调整材质等),记录当前场景的状态或操作信息到历史栈中。
  • 撤销操作:从历史栈中取出最近一次操作的状态,恢复场景。
  • 重做功能(可选):维护一个"重做栈"来支持撤销后的重做。

2. 实现步骤

(1) 维护历史栈

ini 复制代码
// 历史记录栈 let historyStack = []; 
// 重做栈(可选) let redoStack = []; 
// 当前场景对象 const scene = new THREE.Scene();

(2) 深拷贝场景状态

  • 注意点 每次操作后,将当前场景的状态保存到历史栈中。由于Three.js对象是引用类型,需要深拷贝(注意:直接使用 JSON.parse(JSON.stringify(...)) 可能无法处理复杂对象,需自定义深拷贝逻辑)。
yaml 复制代码
function saveSceneState() { 
    // 深拷贝当前场景的状态(仅示例,实际需根据需求调整)
    const state = { 
        objects: scene.children.map((obj) => ({ 
            uuid: obj.uuid, 
            position: { x: obj.position.x, y: obj.position.y, z: obj.position.z }, 
            rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, 
            // 其他属性如材质、缩放等 
       })
    ), 
    historyStack.push(state); 
    redoStack = []; // 清空重做栈 }
}; 

(3) 撤销操作

  • 从历史栈中取出上一个状态,恢复场景:
scss 复制代码
function undo() {
  if (historyStack.length === 0) return;

  const previousState = historyStack.pop();
  redoStack.push(cloneSceneState()); // 保存当前状态到重做栈

  // 清除当前场景
  while (scene.children.length > 0) {
    scene.remove(scene.children[0]);
  }

  // 恢复场景状态
  previousState.objects.forEach((objData) => {
    const obj = findObjectByUUID(objData.uuid); // 需要自定义查找逻辑
    if (obj) {
      obj.position.set(objData.position.x, objData.position.y, objData.position.z);
      obj.rotation.set(objData.rotation.x, objData.rotation.y, objData.rotation.z);
      scene.add(obj);
    }
  });
}

(4) 重做操作(可选)

scss 复制代码
function redo() {
  if (redoStack.length === 0) return;

  const nextState = redoStack.pop();
  historyStack.push(cloneSceneState()); // 保存当前状态到历史栈

  // 清除当前场景
  while (scene.children.length > 0) {
    scene.remove(scene.children[0]);
  }

  // 恢复重做状态
  nextState.objects.forEach((objData) => {
    const obj = findObjectByUUID(objData.uuid);
    if (obj) {
      obj.position.set(objData.position.x, objData.position.y, objData.position.z);
      obj.rotation.set(objData.rotation.x, objData.rotation.y, objData.rotation.z);
      scene.add(obj);
    }
  });
}

3. 优化与注意事项

(1) 避免频繁深拷贝

复制代码
如果场景较大,直接深拷贝整个场景会消耗性能。可以改为记录增量操作(例如记录添加/删除物体、位置变化的具体参数),而非保存完整状态。
示例:记录每次操作的逆操作(如移动物体前记录旧位置,撤销时直接恢复旧位置)。

(2) 处理复杂对象

scss 复制代码
Three.js的 Geometry、Material、Texture 等资源需要手动调用 .dispose() 释放内存(参考知识库中的销毁方法)。在撤销时,如果删除了某个物体,需确保其资源被正确释放。
示例:
function removeObject(obj) {
      // 记录操作
      historyStack.push({
      type: "remove",
      object: obj,
      });
      scene.remove(obj);
      // 释放资源
      obj.geometry.dispose();
      obj.material.dispose();
}

(3) 键盘事件绑定

  • 监听 Ctrl+ZCtrl+Y(或 Ctrl+Shift+Z)触发撤销/重做:
scss 复制代码
window.addEventListener("keydown", (e) => {
  if ((e.ctrlKey || e.metaKey) && e.code === "KeyZ") {
    e.preventDefault();
    if (e.shiftKey || e.ctrlKey) {
      redo(); // Ctrl+Shift+Z 或 Ctrl+Y
    } else {
      undo(); // Ctrl+Z
    }
  }
});

(4) 历史栈大小限制

  • 避免无限增长的栈占用内存,可设置最大历史长度:
scss 复制代码
const MAX_HISTORY = 20;
function saveSceneState() {
  if (historyStack.length >= MAX_HISTORY) {
    historyStack.shift(); // 移除最旧的状态
  }
  // 保存新状态
}

4. 完整示例代码

  • 以下是一个简化版的示例,仅支持撤销物体位置的修改:
ini 复制代码
let historyStack = [];
let redoStack = [];
const scene = new THREE.Scene();

function saveSceneState() {
  const state = {
    objects: scene.children.map((obj) => ({
      uuid: obj.uuid,
      position: obj.position.clone(),
      rotation: obj.rotation.clone(),
    })),
  };
  historyStack.push(state);
  redoStack = [];
}

function undo() {
  if (historyStack.length === 0) return;
  const previousState = historyStack.pop();
  redoStack.push({
    objects: scene.children.map((obj) => ({
      uuid: obj.uuid,
      position: obj.position.clone(),
      rotation: obj.rotation.clone(),
    })),
  });

  scene.children.forEach((obj) => {
    const objState = previousState.objects.find((o) => o.uuid === obj.uuid);
    if (objState) {
      obj.position.copy(objState.position);
      obj.rotation.copy(objState.rotation);
    }
  });
}

// 绑定键盘事件
window.addEventListener("keydown", (e) => {
  if ((e.ctrlKey || e.metaKey) && e.code === "KeyZ") {
    e.preventDefault();
    if (e.shiftKey) {
      redo(); // Ctrl+Shift+Z
    } else {
      undo(); // Ctrl+Z
    }
  }
});

5. 结合知识库中的销毁逻辑

  • 如果需要在撤销时正确释放资源(如删除物体时释放 geometry 和 material),需在操作中显示调用 .dispose():
ini 复制代码
function removeObject(obj) {
  historyStack.push({
    type: "remove",
    object: obj,
  });
  scene.remove(obj);
  obj.geometry.dispose();
  obj.material.dispose();
  obj = null;
}
相关推荐
zwjapple2 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20204 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem5 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊5 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术5 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing5 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止6 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall6 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴6 小时前
简单入门Python装饰器
前端·python
袁煦丞6 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作