选取、批量编辑与变换
编辑器把每个被选中的体素保存在源网格坐标中,并在模型上显示逐格半透明高亮。选区操作均以整数格为单位;它们会写入一条可撤销历史并只刷新受影响的 Chunk,而不是整模型重新导入。
单点选择与取色
先切换模式,再让用户直接触控模型:
this.editor.setMode(VoxelEditMode.SELECT); // 单击增减一个选中体素
this.editor.setMode(VoxelEditMode.PICK); // 单击吸取颜色
this.editor.setMode(VoxelEditMode.ADD); // 命中面外侧放置
this.editor.setMode(VoxelEditMode.REPLACE);
this.editor.setSelectedColor('#3A86FF');
setSelectedSymbol(symbol) 只接受当前调色板已存在的符号;setSelectedColor(hex) 接受 #RGB 或 #RRGGBB,自动复用相同颜色或新增一个内部符号。新增符号会出现在 VoxelEditorState.palette,并随 JSON/VFP 保存。
开启 setSymmetryX(true) 后,ADD、DELETE、REPLACE 会同时尝试写入 X 轴镜像格;镜像目标越界、与原格重合或不符合该操作条件时会被安全忽略。
三种程序化选区
这些方法适合工具栏按钮、命令菜单或脚本化编辑。返回值均为当前选区体素数;未进入编辑或没有匹配项时返回 0。
| 方法 | 签名 | 选择规则 |
|---|---|---|
| 全选 | selectAll(): number | 选择全部非空体素。 |
| 按颜色选择 | selectByColor(color?: string, append?: boolean): number | color 可为调色板符号或 #RRGGBB;默认使用当前颜色。 |
| 三维坐标盒 | selectByBox(min, max, append?: boolean): number | 选择包含端点在内的源网格轴对齐盒中的非空体素。 |
| 清空 | clearSelection(): void | 仅清空选择,不修改模型。 |
// 选择某个颜色,再把它和已选择内容合并。
this.editor.selectByColor('#6B9E47');
this.editor.selectByColor('#D995A8', true);
// 坐标会被取整并钳制到 0..gridSize-1,两个端点顺序不限。
this.editor.selectByBox(
new VoxelCoordinate(10, 0, 6),
new VoxelCoordinate(34, 22, 28)
);
可以用 getSelectionVoxels() 获取副本、getSelectionSize() 获取数量、getSelectionBounds() 获取包含式 min/max 边界。返回的数组和对象可安全用于 UI,不要修改后期待它们反写模型。
滑动连续选择
将模式切换为 SELECT_BRUSH 后,用户从模型的可见表面按下并单指滑过,即可连续加入选区。组件先判断按下点是否命中体素:命中才开始笔画;从 Viewer 空白画布按下并拖动时仍是普通相机环绕,不会修改选区、创建 Undo 或触发刷选回调。笔画中组件按屏幕轨迹采样并去重,保证每个新命中的体素只处理一次;选区高亮以帧为单位合并刷新,因此滑动过程不会为每个体素分别触发一次高亮重建。
const options = new VoxelEditorOptions();
options.selectBrushAppend = true; // 默认值:保留已有选区并追加
options.onSelectBrushHit = (x: number, y: number, z: number): void => {
// 可在这里触发轻量触感、计数或业务 UI;不要在回调中发起编辑命令。
console.info(`brush hit: ${x}, ${y}, ${z}`);
};
this.editor.setMode(VoxelEditMode.SELECT_BRUSH);
selectBrushAppend=true(默认)会保留已有选区;设为false时,每一笔开始前先清空选区,再加入本笔命中体素。- 该模式只会加入选区,不会像
SELECT点击那样切换/移除已有成员;移除选区成员可切回SELECT单击,或调用clearSelection()后重新选择。 onSelectBrushHit只在本笔新加入体素时触发一次。回调仅用于宿主反馈;其异常不会中断滑选。- 第二根手指落下会结束当前笔画,随后仍可正常双指平移或缩放。
可选的“刷选 → 双指相机”桥接
如果产品需要在刷选时向自定义工具栏显示手势状态,或希望允许双指旋转,可在 Controller 上显式启用桥接。它默认关闭,因此已有 SELECT_BRUSH 页面升级后不会改变任何手感。
import {
VoxelSelectionGestureOptions,
VoxelSelectionGesturePhase,
VoxelSelectionGestureState
} from 'voxel-kit';
const gestures = new VoxelSelectionGestureOptions();
gestures.enableBrush = true;
gestures.enableTwoFingerCamera = true;
gestures.twoFingerPan = true;
gestures.twoFingerZoom = true;
gestures.twoFingerRotation = true;
gestures.brushAppend = true;
gestures.onBrushStateChanged = (state: VoxelSelectionGestureState): void => {
if (state.phase === VoxelSelectionGesturePhase.BRUSHING) {
// 显示“正在选择”。
}
};
this.controller.setSelectionGestureOptions(gestures);
状态会按 IDLE → BRUSHING → CAMERA → IDLE 转换;模型切换、工具切换、调用 resetSelectionGestureState() 或更新手势锁时,会短暂通知 CANCELLED 后回到 IDLE。第二指落下时,当前刷选会同步收尾;两个原触点在全部抬起前不会自动恢复刷选,因此相机平移、缩放和旋转不会误改选区或产生 Undo 记录。横向单指 Orbit 在预览、编辑及空白起手时方向一致;自动旋转会在任意单/双指或框选触摸期间暂停,并在全部触点结束后从当前姿态继续。
brushAppend 仅在启用该桥接时覆盖本次笔画的追加策略;未启用时继续使用 VoxelEditorOptions.selectBrushAppend。回调只应用于 UI/触感提示,禁止在回调内同步调用编辑、保存或导入操作。
如需由宿主自己的工具、手势或覆盖层驱动选择,可调用 pickVoxelAt(screenX, screenY) 获取该 Viewer 内容区域内坐标对应的可见表面体素;详见 VoxelEditorController。
屏幕框选
将模式切换为 BOX_SELECT 后,用户可以从 Viewer 画布的任意位置按下并拖到任意位置:
- 拖动中显示半透明矩形和实时逐格高亮预览;预览使用低采样,避免拖动卡顿。
- 松手后使用更密的逐像素视线采样收敛最终结果。
- 每条视线只取最先命中的可见体素,因此不会把被前景遮挡的背面体素加入选区。
该模式单指拖动不会旋转模型。如果要重新旋转,切回 VIEW 或其他非框选模式。屏幕框选只负责可见表面;若需选取网格内部或不在当前视角内的体素,请使用 selectByBox()。
批量删除与换色
| 方法 | 结果 | 返回值 |
|---|---|---|
deleteSelection() | 删除全部选中体素。 | 实际删除数量。 |
setSelectionSymbol(symbol) | 用已有调色板符号批量换色。 | 实际修改数量。 |
setSelectionColor(hex) | 用任意 #RGB / #RRGGBB 批量换色。 | 实际修改数量。 |
if (this.editor.getSelectionSize() > 0) {
this.editor.setSelectionColor('#F1C7D6');
// 或 this.editor.deleteSelection();
}
空选区不会抛错,操作返回 0 并通过状态 message 提示。批量操作会作为一条 Undo 历史记录。
选区变换
| 方法 | 签名 | 行为 |
|---|---|---|
| 移动 | moveSelectionBy(dx?, dy?, dz?): number | 整数格平移,移动后仍选中移动后的体素。 |
| 复制 | duplicateSelection(offset?): number | 保留原选区,复制品成为当前选区。 |
| 镜像 | mirrorSelection(axis?): number | 围绕选区自身的包含式边界原地镜像。 |
| 旋转 | rotateSelection(axis?, quarterTurns?): number | 围绕选区自身边界按 90° 的整数倍旋转。 |
| 对齐 | alignSelection(mode?): number | 将每个选中体素压到选区的某个最小、中间或最大坐标平面。 |
// 复制一份到右侧两格。
this.editor.duplicateSelection(new VoxelCoordinate(2, 0, 0));
// 将当前选区绕 Y 轴顺时针旋转 90°,然后压到自身底部平面。
this.editor.rotateSelection(VoxelAxis.Y, 1);
this.editor.alignSelection(VoxelSelectionAlign.MIN_Y);
变换前会进行两类安全检查:
- 任何目标格超出
0..gridSize-1时,整次变换取消; - 目标若覆盖未被选中的实心体素,整次变换取消。
因此返回 0 既可能表示空选区,也可能表示越界/冲突;应读取 getState().message 显示具体原因。选区内部体素相互重叠是允许的,最终会合并为一个目标格。
撤销、重做与历史上限
if (this.state?.canUndo) this.editor.undo();
if (this.state?.canRedo) this.editor.redo();
每次添加、删除、替换、批量换色和选区变换记为一条历史记录;选择本身不进入历史。VoxelEditorOptions.maxHistory 默认 100,超出后最早的记录被丢弃。重新导入模型、退出编辑或再次进入一个新场景会清空历史。
选区高亮仅是视觉层,不属于模型数据,也不会写进 JSON/VFP。