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

在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;
}
相关推荐
巴拉巴拉~~几秒前
KMP 算法通用图表组件:KmpChartWidget 多维度可视化 + PMT 表渲染 + 性能对比
前端·javascript·microsoft
智算菩萨7 分钟前
基于spaCy的英文自然语言处理系统:低频词提取与高级文本分析
前端·javascript·easyui
刘一说17 分钟前
Vue单页应用(SPA)开发全解析:从原理到最佳实践
前端·javascript·vue.js
疯狂成瘾者18 分钟前
前端vue核心知识点
前端·javascript·vue.js
Laravel技术社区1 小时前
用PHP8实现斗地主游戏,实现三带一,三带二,四带二,顺子,王炸功能(第二集)
前端·游戏·php
m0_738120722 小时前
应急响应——知攻善防Web-3靶机详细教程
服务器·前端·网络·安全·web安全·php
程序员爱钓鱼9 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
PineappleCoder9 小时前
工程化必备!SVG 雪碧图的最佳实践:ID 引用 + 缓存友好,无需手动算坐标
前端·性能优化
JIngJaneIL10 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
敲敲了个代码10 小时前
隐式类型转换:哈基米 == 猫 ? true :false
开发语言·前端·javascript·学习·面试·web