支持布尔操作按帧范围执行

- 区域合并和重叠区域去除新增“按帧范围选择”,复用底部时间轴范围选择并在执行前二次确认

- 布尔操作范围只处理所选帧内存在对应传播链的区域,范围外传播 mask 保持不变

- 自动传播范围选择时在顶栏显示传播权重,以及相对参考帧的向前/向后传播帧数

- Canvas 将传播链布尔操作委托给工作区统一处理范围选择,同时保留当前帧/所有传播帧快捷操作

- 增加 CanvasArea、VideoWorkspace 回归测试,覆盖布尔操作范围选择、范围执行和自动传播方向摘要

- 更新 AGENTS 与前端审计、需求冻结、设计冻结、测试计划文档
This commit is contained in:
2026-05-03 23:30:47 +08:00
parent 5ae1d15336
commit b97c00900c
9 changed files with 405 additions and 30 deletions

View File

@@ -993,6 +993,68 @@ describe('CanvasArea', () => {
}));
});
it('can hand propagated boolean operations to the workspace frame range selector', () => {
const onRequestBooleanFrameRange = vi.fn();
useStore.setState({
masks: [
{
id: 'annotation-1',
annotationId: '1',
frameId: 'frame-1',
pathData: 'M 10 10 L 60 10 L 60 60 L 10 60 Z',
label: 'A',
color: '#06b6d4',
segmentation: [[10, 10, 60, 10, 60, 60, 10, 60]],
},
{
id: 'annotation-2',
annotationId: '2',
frameId: 'frame-1',
pathData: 'M 50 50 L 100 50 L 100 100 L 50 100 Z',
label: 'A',
color: '#06b6d4',
segmentation: [[50, 50, 100, 50, 100, 100, 50, 100]],
},
{
id: 'annotation-10',
annotationId: '10',
frameId: 'frame-2',
pathData: 'M 12 12 L 62 12 L 62 62 L 12 62 Z',
label: 'A',
color: '#06b6d4',
segmentation: [[12, 12, 62, 12, 62, 62, 12, 62]],
metadata: { source: 'sam2.1_hiera_tiny_propagation', source_annotation_id: 1, propagation_seed_key: 'annotation:1' },
},
{
id: 'annotation-20',
annotationId: '20',
frameId: 'frame-2',
pathData: 'M 52 52 L 102 52 L 102 102 L 52 102 Z',
label: 'A',
color: '#06b6d4',
segmentation: [[52, 52, 102, 52, 102, 102, 52, 102]],
metadata: { source: 'sam2.1_hiera_tiny_propagation', source_annotation_id: 2, propagation_seed_key: 'annotation:2' },
},
],
});
render(<CanvasArea activeTool="area_merge" frame={frame} onRequestBooleanFrameRange={onRequestBooleanFrameRange} />);
const paths = screen.getAllByTestId('konva-path');
fireEvent.click(paths[0]);
fireEvent.click(paths[1]);
fireEvent.click(screen.getByRole('button', { name: '合并选中' }));
fireEvent.click(screen.getByRole('button', { name: '按帧范围选择' }));
expect(onRequestBooleanFrameRange).toHaveBeenCalledWith(expect.objectContaining({
operation: 'area_merge',
currentFrameId: 'frame-1',
candidateFrameIds: expect.arrayContaining(['frame-1', 'frame-2']),
selectedMaskIds: ['annotation-1', 'annotation-2'],
execute: expect.any(Function),
}));
expect(useStore.getState().masks.map((mask) => mask.id).sort()).toEqual(['annotation-1', 'annotation-10', 'annotation-2', 'annotation-20']);
});
it('removes overlap from the primary selected mask with polygon difference', () => {
useStore.setState({
masks: [