修复布尔范围操作重复执行
- 重新开始区域合并或重叠区域去除时取消旧的顶栏按帧范围请求 - 防止先选择按帧范围、再处理当前帧后还能继续执行旧范围合并 - 补充 VideoWorkspace 回归测试覆盖旧范围请求被取消的路径 - 更新项目指南、设计冻结和测试计划中的互斥交互说明
This commit is contained in:
@@ -24,6 +24,7 @@ interface CanvasAreaProps {
|
||||
totalFrames?: number;
|
||||
onRequestDeleteMasks?: (maskIds: string[]) => void;
|
||||
onRequestBooleanFrameRange?: (request: BooleanFrameRangeRequest) => void;
|
||||
onBooleanOperationStart?: () => void;
|
||||
onDeleteMaskAnnotations?: (annotationIds: string[]) => Promise<void> | void;
|
||||
}
|
||||
|
||||
@@ -438,6 +439,7 @@ export function CanvasArea({
|
||||
totalFrames,
|
||||
onRequestDeleteMasks,
|
||||
onRequestBooleanFrameRange,
|
||||
onBooleanOperationStart,
|
||||
onDeleteMaskAnnotations,
|
||||
}: CanvasAreaProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -1528,6 +1530,7 @@ export function CanvasArea({
|
||||
const handleBooleanOperation = async () => {
|
||||
const frameSelection = collectBooleanOperationFrameIds();
|
||||
if (!frameSelection) return;
|
||||
onBooleanOperationStart?.();
|
||||
const { currentFrameId, targetFrameIds } = frameSelection;
|
||||
if (targetFrameIds.size > 1) {
|
||||
setPendingBooleanFrameIds(Array.from(targetFrameIds));
|
||||
|
||||
@@ -1207,6 +1207,94 @@ describe('VideoWorkspace', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('cancels a pending boolean frame range when the user starts a new current-frame merge', async () => {
|
||||
apiMock.getProjectFrames.mockResolvedValueOnce([
|
||||
{ id: 10, project_id: 1, frame_index: 0, image_url: '/frame-0.jpg', width: 640, height: 360 },
|
||||
{ id: 11, project_id: 1, frame_index: 1, image_url: '/frame-1.jpg', width: 640, height: 360 },
|
||||
]);
|
||||
|
||||
render(<VideoWorkspace />);
|
||||
await waitFor(() => expect(useStore.getState().frames).toHaveLength(2));
|
||||
act(() => {
|
||||
useStore.setState({
|
||||
masks: [
|
||||
{
|
||||
id: 'annotation-1',
|
||||
annotationId: '1',
|
||||
frameId: '10',
|
||||
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]],
|
||||
saved: true,
|
||||
saveStatus: 'saved',
|
||||
},
|
||||
{
|
||||
id: 'annotation-2',
|
||||
annotationId: '2',
|
||||
frameId: '10',
|
||||
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]],
|
||||
saved: true,
|
||||
saveStatus: 'saved',
|
||||
},
|
||||
{
|
||||
id: 'annotation-10',
|
||||
annotationId: '10',
|
||||
frameId: '11',
|
||||
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]],
|
||||
saved: true,
|
||||
saveStatus: 'saved',
|
||||
metadata: { source: 'sam2.1_hiera_tiny_propagation', source_annotation_id: 1, source_mask_id: 'annotation-1', propagation_seed_key: 'annotation:1' },
|
||||
},
|
||||
{
|
||||
id: 'annotation-20',
|
||||
annotationId: '20',
|
||||
frameId: '11',
|
||||
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]],
|
||||
saved: true,
|
||||
saveStatus: 'saved',
|
||||
metadata: { source: 'sam2.1_hiera_tiny_propagation', source_annotation_id: 2, source_mask_id: 'annotation-2', propagation_seed_key: 'annotation:2' },
|
||||
},
|
||||
],
|
||||
activeTool: 'area_merge',
|
||||
});
|
||||
});
|
||||
apiMock.getProjectAnnotations.mockResolvedValue([
|
||||
{ id: 1, frame_id: 10 },
|
||||
{ id: 2, frame_id: 10 },
|
||||
{ id: 10, frame_id: 11 },
|
||||
{ id: 20, frame_id: 11 },
|
||||
]);
|
||||
|
||||
const paths = await screen.findAllByTestId('konva-path');
|
||||
fireEvent.click(paths[0]);
|
||||
fireEvent.click(paths[1]);
|
||||
fireEvent.click(screen.getByRole('button', { name: '合并选中' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '按帧范围选择' }));
|
||||
expect(screen.getByRole('button', { name: '确认区域合并' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '合并选中' }));
|
||||
expect(screen.queryByRole('button', { name: '确认区域合并' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '只处理当前帧' }));
|
||||
|
||||
await waitFor(() => {
|
||||
const ids = useStore.getState().masks.map((mask) => mask.id).sort();
|
||||
expect(ids).toEqual(['annotation-1', 'annotation-10', 'annotation-20']);
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: '确认区域合并' })).not.toBeInTheDocument();
|
||||
expect(apiMock.deleteAnnotation).toHaveBeenCalledWith('2');
|
||||
expect(apiMock.deleteAnnotation).not.toHaveBeenCalledWith('20');
|
||||
});
|
||||
|
||||
it('exports only the current frame when current image scope is selected', async () => {
|
||||
apiMock.getProjectFrames.mockResolvedValueOnce([
|
||||
{ id: 10, project_id: 1, frame_index: 0, image_url: '/frame-0.jpg', width: 640, height: 360 },
|
||||
|
||||
@@ -1015,6 +1015,17 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
||||
setStatusMessage(`请选择${booleanOperationLabel(request.operation)}帧范围,再点击“确认${booleanOperationLabel(request.operation)}”`);
|
||||
}, [frameNumberById]);
|
||||
|
||||
const clearPendingBooleanRangeSelection = useCallback(() => {
|
||||
setPendingBooleanRangeRequest(null);
|
||||
setPendingBooleanRangeConfirm(null);
|
||||
if (rangeSelectionMode === 'boolean') {
|
||||
setRangeSelectionMode(null);
|
||||
setIsPropagationRangeSelecting(false);
|
||||
setHasExplicitPropagationRange(false);
|
||||
setStatusMessage('');
|
||||
}
|
||||
}, [rangeSelectionMode]);
|
||||
|
||||
const handleConfirmBooleanFrameRangeOperation = useCallback(() => {
|
||||
if (!pendingBooleanRangeRequest || frames.length === 0) return;
|
||||
const clampRangeFrameNumber = (value: number) => {
|
||||
@@ -2114,6 +2125,7 @@ export function VideoWorkspace({ onNavigateToAI }: { onNavigateToAI?: () => void
|
||||
totalFrames={totalFrames}
|
||||
onRequestDeleteMasks={(maskIds) => void handleDeleteSelectedMasks(maskIds)}
|
||||
onRequestBooleanFrameRange={handleBooleanFrameRangeRequest}
|
||||
onBooleanOperationStart={clearPendingBooleanRangeSelection}
|
||||
onDeleteMaskAnnotations={handleDeleteMaskAnnotations}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user