跳到主要内容

选取、批量编辑与变换

编辑器把每个被选中的体素保存在源网格坐标中,并在模型上显示逐格半透明高亮。选区操作均以整数格为单位;它们会写入一条可撤销历史并只刷新受影响的 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): numbercolor 可为调色板符号或 #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 画布的任意位置按下并拖到任意位置:

  1. 拖动中显示半透明矩形和实时逐格高亮预览;预览使用低采样,避免拖动卡顿。
  2. 松手后使用更密的逐像素视线采样收敛最终结果。
  3. 每条视线只取最先命中的可见体素,因此不会把被前景遮挡的背面体素加入选区。

该模式单指拖动不会旋转模型。如果要重新旋转,切回 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。