命令机制
命令机制主要用来编辑过程中对操作进行撤销、重做等操作。它是App中CommandModule的功能。
常用的命令机制方法
| 方法名 | 描述 |
|---|---|
| commandHistory | 命令历史记录 |
| clearCommands | 清空命令 |
| executeCommand | 执行命令,自动执行 |
| executeAllCommands | 执行所有命令,自动执行 |
| executeManyCommands | 执行多个命令,自动执行 |
| addCommand | 增加命令,不会自动执行 |
| addAllCommands | 增加全部命令——⚠️ 实现会包成 MultiCmdsCommand 入栈并立即执行每个子命令(子命令的 autoExecute=false 被忽略),且子命令不会注入 app(依赖 this.app 的自定义命令会抛错,executeAllCommands/executeManyCommands 同);只入栈不执行请逐个 addCommand |
| addManyCommands | 增加多个命令——⚠️ 同 addAllCommands:会立即执行各子命令,只入栈不执行请逐个 addCommand |
| undo | 撤销 |
| redo | 还原 |
一些封装的命令
| 命令 | 描述 |
|---|---|
| SetPositionCommand | 设置位置 |
| SetRotationCommand | 设置旋转 |
| SetScaleCommand | 设置缩放 |
| SetValueCommand | 给对象设置值 |
| SetDataCommand | 给对象SetData |
| SetMaterialMapCommand | 给对象设置材质 |
| AddObject3DCommand | 增加对象 |
| PushValueCommand | 给一个容器加一个值 |
| CallFunctionCommand | 执行一个函数 |
| MultiCmdsCommand | 多个命令一起执行 |
示例
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
},
control: { leftButtonPan: true } // 设置为左键用于平移 (同时右键将用于旋转) 和地图2d使用习惯一样
})
let box = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({
color: "#f00",
side: THREE.DoubleSide
});
let mesh = new THREE.Mesh(box, material);
app.scene.add(mesh)
const ui = await app.getConfigPane({ title: "输入参数", style: { width: "250px"}});
let gui = new vjmap3d.UiPanelJsonConfig();
gui.addButton("移动位置", () => {
let oldPos = mesh.position.clone();
let newPos = vjmap3d.toVector3(vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5}))
mesh.position.copy(newPos);
app.addCommand(new vjmap3d.SetPositionCommand(mesh, newPos.clone(), oldPos));
// 如果用 executeCommand 增加命令的同时,还会执行设置命令 所以上面的两行,可以用下面一行代替
//app.executeCommand(new vjmap3d.SetPositionCommand(mesh, newPos.clone(), oldPos));
})
gui.addButton("撤销", () => {
app.undo()
})
gui.addButton("重做", () => {
app.redo()
})
gui.addButton("清空命令", () => {
app.clearCommands()
})
gui.toJson().forEach(c => ui.appendChild(c));
提示
默认情况下支持快捷键
Ctrl + Z 撤销
Ctrl + Y 重做
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
},
control: { leftButtonPan: true } // 设置为左键用于平移 (同时右键将用于旋转) 和地图2d使用习惯一样
})
let box = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({
color: "#f00",
side: THREE.DoubleSide
});
let mesh = new THREE.Mesh(box, material);
app.scene.add(mesh)
const ui = await app.getConfigPane({ title: "输入参数", style: { width: "250px"}});
let gui = new vjmap3d.UiPanelJsonConfig();
gui.addButton("移动位置", () => {
let oldPos = mesh.position.clone();
let newPos = vjmap3d.toVector3(vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5}))
mesh.position.copy(newPos);
app.addCommand(new vjmap3d.SetPositionCommand(mesh, newPos.clone(), oldPos));
// 如果用 executeCommand 增加命令的同时,还会执行设置命令 所以上面的两行,可以用下面一行代替
//app.executeCommand(new vjmap3d.SetPositionCommand(mesh, newPos.clone(), oldPos));
})
gui.addButton("撤销", () => {
app.undo()
})
gui.addButton("重做", () => {
app.redo()
})
gui.addButton("清空命令", () => {
app.clearCommands()
})
gui.toJson().forEach(c => ui.appendChild(c));
自定义命令
// 增加对象命令
class MyAddObject3DCommand extends vjmap3d.BaseCommand {
constructor (object) {
super();
this.object = object;
}
execute () {
if (!this.object && this.json) {
// 如果是redo过来的
let loader = new THREE.ObjectLoader();
this.object = loader.parse(this.json);
}
if (this.object) {
this.app.scene.add(this.object);
}
}
undo () {
if (this.object) {
this.json = this.object.toJSON(); // 保存对象的json
this.object.parent.remove(this.object)
this.object = null;
}
}
redo () {
this.execute();
}
}
let box = new THREE.BoxGeometry(Math.random() * 3 + 0.5, Math.random() * 3 + 0.5, Math.random() * 3 + 0.5)
const material = new THREE.MeshStandardMaterial({
color: vjmap3d.randColor()
});
let mesh = new THREE.Mesh(box, material);
mesh.position.copy(vjmap3d.toVector3(vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5})))
app.executeCommand(new MyAddObject3DCommand(mesh));
// 上面的语句等同行下面的两句(可以先mesh加进场景)
//app.scene.add(mesh);
//app.addCommand(new MyAddObject3DCommand(mesh));
// 增加对象命令
class MyAddObject3DCommand extends vjmap3d.BaseCommand {
constructor (object) {
super();
this.object = object;
}
execute () {
if (!this.object && this.json) {
// 如果是redo过来的
let loader = new THREE.ObjectLoader();
this.object = loader.parse(this.json);
}
if (this.object) {
this.app.scene.add(this.object);
}
}
undo () {
if (this.object) {
this.json = this.object.toJSON(); // 保存对象的json
this.object.parent.remove(this.object)
this.object = null;
}
}
redo () {
this.execute();
}
}
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
},
control: { leftButtonPan: true } // 设置为左键用于平移 (同时右键将用于旋转) 和地图2d使用习惯一样
})
const ui = await app.getConfigPane({ title: "输入参数", style: { width: "250px"}});
let gui = new vjmap3d.UiPanelJsonConfig();
gui.addButton("增加实体", () => {
let box = new THREE.BoxGeometry(Math.random() * 3 + 0.5, Math.random() * 3 + 0.5, Math.random() * 3 + 0.5)
const material = new THREE.MeshStandardMaterial({
color: vjmap3d.randColor()
});
let mesh = new THREE.Mesh(box, material);
mesh.position.copy(vjmap3d.toVector3(vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5})))
app.executeCommand(new MyAddObject3DCommand(mesh));
// 上面的语句等同行下面的两句(可以先mesh加进场景)
//app.scene.add(mesh);
//app.addCommand(new MyAddObject3DCommand(mesh));
})
gui.addButton("撤销", () => {
app.undo()
})
gui.addButton("重做", () => {
app.redo()
})
gui.addButton("清空命令", () => {
app.clearCommands()
})
gui.toJson().forEach(c => ui.appendChild(c));
常见坑位
-
undo()/redo()场景过滤循环越界崩溃(源码级 bug)CommandHistory.undo()的实现是for(let c = undos.length-1; c >= 0; c++)——自增方向写反了(应为c--)。它实际只检查栈顶一条命令:若该命令的owerScene != app.scene(例如命令创建于另一个场景、或场景被clearScenes()/重建后app.scene引用已变),循环继续读undos[len]→undefined→ 抛Cannot read properties of undefined (reading 'owerScene')。redo()同病。- 正常单场景流程不触发:栈顶命令的
owerScene总是当前场景。 - 触发场景:多场景切换、
clearScenes()后再 undo、手动 push 未设owerScene的命令。 - 崩溃不改历史状态(undos 原样保留),可将出错命令的
owerScene改回app.scene或app.clearCommands()恢复。
- 正常单场景流程不触发:栈顶命令的
-
addCommand入栈但不执行——文档"不会自动执行"属实,但注意后果:undo()会撤销一条从未执行过的命令(对多数命令是空操作,对依赖已生效状态的命令可能产生意外行为)。需要"先执行再入栈"语义时用executeCommand。 -
历史长度字段拼写为
MaxHistroyLength(Histroy 缺个 o,commandHistory.MaxHistroyLength与构造参数maxHistroyLength均是此拼写),写MaxHistoryLength不生效。 -
app.executeCommand(null)抛Cannot set properties of null (setting 'app')——参数必须是对象,不做空值防护。