feat: 完善标注删除、AI 框选与视频传播交互
功能增加: - 在工作区增加按范围传播和传播全部可达入口,支持选中区域或当前帧全部 mask 作为 seed,并按前后帧范围调用 SAM2 传播后刷新已保存标注。 - 在 AI 智能分割中接入框选提示,支持 box prompt 以及 box + 正/反向点的 interactive prompt 细化流程。 - 在 AI 智能分割中增加提示点删除、最近锚点删除、清空锚点、选中 AI 候选删除和 Delete/Backspace 快捷删除。 - 在项目库删除项目后同步清理当前项目、帧、mask 与选区状态,避免删除后工作区残留旧数据。 - 将时间进度条上的已编辑帧提示改为覆盖在进度条上的琥珀色竖线,并保留已编辑帧计数。 - 将 AI 参数文案调整为局部专注模式(自动裁剪无锚区域)和严格除杂模式(自动清理干涉点),仅改善可读性,不改变内部字段。 Bugfix: - 修复 AI 框选工具无实际 prompt 输出的问题。 - 修复多次执行 AI 高精度语义分割时旧候选 mask 叠加显示的问题,改为替换本页 AI 候选。 - 修复删除 AI 候选后选区仍引用已删除 mask 的状态残留。 - 修复进度条当前帧提示与已编辑帧提示颜色/语义混淆的问题,当前帧继续由播放进度和缩略图高亮表达。 测试与文档: - 补充 AI 分割框选、候选替换、提示点删除和快捷删除相关测试。 - 补充工作区传播范围、传播全部可达、编辑区域删除和项目删除状态清理测试。 - 更新 README、AGENTS 和 doc 下需求冻结、设计冻结、接口契约、前端审计、实施计划、测试计划,记录当前真实功能和测试覆盖。
This commit is contained in:
@@ -209,6 +209,75 @@ describe('CanvasArea', () => {
|
||||
expect(await screen.findByText(/反向点已排除当前候选区域/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('deletes a workspace SAM2 prompt point before the stage can add another point', async () => {
|
||||
apiMock.predictMask
|
||||
.mockResolvedValueOnce({
|
||||
masks: [
|
||||
{
|
||||
id: 'mask-prompt',
|
||||
pathData: 'M 10 10 L 90 10 L 90 90 Z',
|
||||
label: 'AI Mask',
|
||||
color: '#06b6d4',
|
||||
segmentation: [[10, 10, 90, 10, 90, 90]],
|
||||
bbox: [10, 10, 80, 80],
|
||||
area: 6400,
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
masks: [
|
||||
{
|
||||
id: 'mask-refined',
|
||||
pathData: 'M 20 20 L 80 20 L 80 80 Z',
|
||||
label: 'AI Mask',
|
||||
color: '#06b6d4',
|
||||
segmentation: [[20, 20, 80, 20, 80, 80]],
|
||||
bbox: [20, 20, 60, 60],
|
||||
area: 3600,
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
masks: [
|
||||
{
|
||||
id: 'mask-after-delete',
|
||||
pathData: 'M 30 30 L 70 30 L 70 70 Z',
|
||||
label: 'AI Mask',
|
||||
color: '#06b6d4',
|
||||
segmentation: [[30, 30, 70, 30, 70, 70]],
|
||||
bbox: [30, 30, 40, 40],
|
||||
area: 1600,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { rerender } = render(<CanvasArea activeTool="point_pos" frame={frame} />);
|
||||
const stage = screen.getByTestId('konva-stage');
|
||||
fireEvent.click(stage, { clientX: 120, clientY: 80 });
|
||||
await waitFor(() => expect(apiMock.predictMask).toHaveBeenCalledTimes(1));
|
||||
|
||||
rerender(<CanvasArea activeTool="point_neg" frame={frame} />);
|
||||
fireEvent.click(stage, { clientX: 220, clientY: 140 });
|
||||
await waitFor(() => expect(apiMock.predictMask).toHaveBeenCalledTimes(2));
|
||||
const promptOuterCircles = () => screen.getAllByTestId('konva-circle')
|
||||
.filter((element) => ['#22c55e', '#ef4444'].includes(element.getAttribute('data-fill') || ''));
|
||||
expect(promptOuterCircles()).toHaveLength(2);
|
||||
|
||||
fireEvent.click(promptOuterCircles()[0]);
|
||||
|
||||
await waitFor(() => expect(apiMock.predictMask).toHaveBeenCalledTimes(3));
|
||||
expect(apiMock.predictMask).toHaveBeenLastCalledWith({
|
||||
imageId: 'frame-1',
|
||||
imageWidth: 640,
|
||||
imageHeight: 360,
|
||||
model: 'sam2.1_hiera_tiny',
|
||||
points: [{ x: 220, y: 140, type: 'neg' }],
|
||||
box: undefined,
|
||||
options: { auto_filter_background: true, min_score: 0.05 },
|
||||
});
|
||||
expect(promptOuterCircles()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders only masks that belong to the current frame', () => {
|
||||
useStore.setState({
|
||||
masks: [
|
||||
|
||||
Reference in New Issue
Block a user