# 命令机制
 
  `命令机制`主要用来编辑过程中对`操作`进行`撤销`、`重做`等操作。它是[App](https://vjmap.com/map3d/api/classes/App.html)中[CommandModule](https://vjmap.com/map3d/api/classes/CommandModule.html)的功能。

  ## 常用的命令机制方法

|方法名|描述|
|:---|:---|
|[commandHistory](https://vjmap.com/map3d/api/classes/CommandModule.html#commandHistory)|命令历史记录|
|[clearCommands](https://vjmap.com/map3d/api/classes/CommandModule.html#clearCommands)|清空命令|
|[executeCommand](https://vjmap.com/map3d/api/classes/CommandModule.html#executeCommand)|执行命令，自动执行|
|[executeAllCommands](https://vjmap.com/map3d/api/classes/CommandModule.html#executeAllCommands)|执行所有命令，自动执行|
|[executeManyCommands](https://vjmap.com/map3d/api/classes/CommandModule.html#executeManyCommands)|执行多个命令，自动执行|
|[addCommand](https://vjmap.com/map3d/api/classes/CommandModule.html#addCommand)|增加命令，不会自动执行|
|[addAllCommands](https://vjmap.com/map3d/api/classes/CommandModule.html#addAllCommands)|增加全部命令——⚠️ 实现会包成 `MultiCmdsCommand` 入栈并**立即执行**每个子命令（子命令的 `autoExecute=false` 被忽略），且子命令不会注入 `app`（依赖 `this.app` 的自定义命令会抛错，`executeAllCommands`/`executeManyCommands` 同）；只入栈不执行请逐个 `addCommand`|
|[addManyCommands](https://vjmap.com/map3d/api/classes/CommandModule.html#addManyCommands)|增加多个命令——⚠️ 同 `addAllCommands`：会立即执行各子命令，只入栈不执行请逐个 `addCommand`|
|[undo](https://vjmap.com/map3d/api/classes/CommandModule.html#undo)|撤销|
|[redo](https://vjmap.com/map3d/api/classes/CommandModule.html#redo)|还原|

## 一些封装的命令

|命令|描述|
|:---|:---|
|[SetPositionCommand](https://vjmap.com/map3d/api/classes/SetPositionCommand.html)|设置位置|
|[SetRotationCommand](https://vjmap.com/map3d/api/classes/SetRotationCommand.html)|设置旋转|
|[SetScaleCommand](https://vjmap.com/map3d/api/classes/SetScaleCommand.html)|设置缩放|
|[SetValueCommand](https://vjmap.com/map3d/api/classes/SetValueCommand.html)|给对象设置值|
|[SetDataCommand](https://vjmap.com/map3d/api/classes/SetDataCommand.html)|给对象SetData|
|[SetMaterialMapCommand](https://vjmap.com/map3d/api/classes/SetMaterialMapCommand.html)|给对象设置材质|
|[AddObject3DCommand](https://vjmap.com/map3d/api/classes/AddObject3DCommand.html)|增加对象|
|[PushValueCommand](https://vjmap.com/map3d/api/classes/PushValueCommand.html)|给一个容器加一个值|
|[CallFunctionCommand](https://vjmap.com/map3d/api/classes/CallFunctionCommand.html)|执行一个函数|
|[MultiCmdsCommand](https://vjmap.com/map3d/api/classes/MultiCmdsCommand.html)|多个命令一起执行|

## 示例

```js
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));
```

::: tip 
默认情况下支持快捷键

Ctrl + Z 撤销

Ctrl + Y 重做
:::

<mapFrame>
<pre>
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));
</pre>
</mapFrame>

## 自定义命令

```js
// 增加对象命令
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));
```

<mapFrame>
<pre>
// 增加对象命令
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));
</pre>
</mapFrame>

## 常见坑位

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')`**——参数必须是对象，不做空值防护。
