在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+Z
和Ctrl+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;
}