唯杰地图 logo唯杰地图
VJMAP
EN

命令机制

vjmap3d/references/interaction/command.md 11.2 KB254 行
原文

命令机制主要用来编辑过程中对操作进行撤销、重做等操作。它是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));

常见坑位

  1. 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() 恢复。
  2. addCommand 入栈但不执行——文档"不会自动执行"属实,但注意后果:undo() 会撤销一条从未执行过的命令(对多数命令是空操作,对依赖已生效状态的命令可能产生意外行为)。需要"先执行再入栈"语义时用 executeCommand。

  3. 历史长度字段拼写为 MaxHistroyLength(Histroy 缺个 o,commandHistory.MaxHistroyLength 与构造参数 maxHistroyLength 均是此拼写),写 MaxHistoryLength 不生效。

  4. app.executeCommand(null) 抛 Cannot set properties of null (setting 'app')——参数必须是对象,不做空值防护。