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

在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;
}
相关推荐
颜酱几秒前
Monorepo 架构以及工具选型、搭建
前端·javascript·node.js
oden5 分钟前
ChatGPT不推荐你?7个GEO技巧让AI主动引用你的内容
前端
李游Leo1 小时前
前端安全攻防指南:XSS / CSRF / 点击劫持与常见防护实践(含真实案例拆解)
前端·安全·xss
我命由我123451 小时前
微信开发者工具 - 模拟器分离窗口与关闭分离窗口
前端·javascript·学习·微信小程序·前端框架·html·js
E***q5391 小时前
Vue增强现实开发
前端·vue.js·ar
S***42801 小时前
JavaScript在Web中的Angular
前端·javascript·angular.js
黑幕困兽1 小时前
ehcarts 实现 饼图扇区间隙+透明外描边
前端·echarts
San301 小时前
深入理解 JavaScript 词法作用域链:从代码到底层实现机制
前端·javascript·ecmascript 6
七淮2 小时前
Next.js SEO 优化完整方案
前端·next.js
e***19352 小时前
爬虫学习 01 Web Scraper的使用
前端·爬虫·学习