修复旧传播分离片段联动选择

- 为缺少稳定 lineage 的旧传播结果增加兼容分组 token
- 同一传播来源、来源帧、方向、分类/标签/颜色的分离片段可联动选中高亮
- 同步 Canvas 和工作区传播链 token 逻辑,保持选择和清空链路一致
- 补充 CanvasArea 回归测试覆盖旧传播结果的不连通片段选中
- 更新项目指南和设计冻结文档
This commit is contained in:
2026-05-04 02:01:49 +08:00
parent 81a47cd405
commit 07f3364718
5 changed files with 104 additions and 5 deletions

View File

@@ -508,6 +508,59 @@ describe('CanvasArea', () => {
expect(screen.getAllByTestId('konva-group').map((group) => group.getAttribute('data-opacity'))).toEqual(['0.5', '0.5']);
});
it('selects legacy separated propagated pieces without stable seed ids on the frame', () => {
useStore.setState({
maskPreviewOpacity: 35,
masks: [
{
id: 'annotation-30',
annotationId: '30',
frameId: 'frame-1',
pathData: 'M 10 10 L 30 10 L 30 30 Z',
label: '胆囊',
color: '#facc15',
segmentation: [[10, 10, 30, 10, 30, 30]],
metadata: {
source: 'sam2_propagation',
propagated_from_frame_id: 'seed-frame',
},
},
{
id: 'annotation-31',
annotationId: '31',
frameId: 'frame-1',
pathData: 'M 80 80 L 100 80 L 100 100 Z',
label: '胆囊',
color: '#facc15',
segmentation: [[80, 80, 100, 80, 100, 100]],
metadata: {
source: 'sam2_propagation',
propagated_from_frame_id: 'seed-frame',
},
},
{
id: 'annotation-40',
annotationId: '40',
frameId: 'frame-1',
pathData: 'M 130 130 L 150 130 L 150 150 Z',
label: '肝脏',
color: '#22c55e',
segmentation: [[130, 130, 150, 130, 150, 150]],
metadata: {
source: 'sam2_propagation',
propagated_from_frame_id: 'seed-frame',
},
},
],
});
render(<CanvasArea activeTool="edit_polygon" frame={frame} />);
fireEvent.click(screen.getAllByTestId('konva-path')[0]);
expect(useStore.getState().selectedMaskIds).toEqual(['annotation-30', 'annotation-31']);
expect(screen.getAllByTestId('konva-group').map((group) => group.getAttribute('data-opacity'))).toEqual(['0.5', '0.5', '0.35']);
});
it('does not render stored GT seed points as visible editable handles', () => {
useStore.setState({
masks: [

View File

@@ -82,16 +82,38 @@ function propagationLineageTokens(mask: Mask): Set<string> {
if (mask.annotationId) {
tokens.add(`annotation:${mask.annotationId}`);
}
let hasStablePropagationToken = false;
const sourceAnnotationId = metadataNumber(metadata.source_annotation_id);
if (sourceAnnotationId !== null) {
tokens.add(`annotation:${sourceAnnotationId}`);
hasStablePropagationToken = true;
}
const sourceMaskTokens = propagationSourceMaskTokens(metadata.source_mask_id);
if (sourceMaskTokens.length > 0) {
sourceMaskTokens.forEach((token) => tokens.add(token));
hasStablePropagationToken = true;
}
propagationSourceMaskTokens(metadata.source_mask_id).forEach((token) => tokens.add(token));
if (typeof metadata.propagation_seed_key === 'string' && metadata.propagation_seed_key.length > 0) {
tokens.add(`seed-key:${metadata.propagation_seed_key}`);
hasStablePropagationToken = true;
}
if (typeof metadata.propagation_seed_signature === 'string' && metadata.propagation_seed_signature.length > 0) {
tokens.add(`seed-signature:${metadata.propagation_seed_signature}`);
hasStablePropagationToken = true;
}
if (isPropagationMask(mask) && !hasStablePropagationToken) {
const source = typeof metadata.source === 'string' ? metadata.source : '';
const classKey = mask.classId || mask.className || '';
tokens.add([
'legacy-propagation',
source,
metadata.propagated_from_frame_id ?? '',
metadata.propagated_from_frame_index ?? '',
metadata.propagation_direction ?? '',
classKey,
mask.label || '',
mask.color || '',
].join(':'));
}
return tokens;
}

View File

@@ -287,14 +287,38 @@ const propagationLineageTokens = (mask: Mask): Set<string> => {
const metadata = mask.metadata || {};
const tokens = new Set<string>([`mask:${mask.id}`]);
if (mask.annotationId) tokens.add(`annotation:${mask.annotationId}`);
let hasStablePropagationToken = false;
const sourceAnnotationId = metadataNumber(metadata.source_annotation_id);
if (sourceAnnotationId !== null) tokens.add(`annotation:${sourceAnnotationId}`);
propagationSourceMaskTokens(metadata.source_mask_id).forEach((token) => tokens.add(token));
if (sourceAnnotationId !== null) {
tokens.add(`annotation:${sourceAnnotationId}`);
hasStablePropagationToken = true;
}
const sourceMaskTokens = propagationSourceMaskTokens(metadata.source_mask_id);
if (sourceMaskTokens.length > 0) {
sourceMaskTokens.forEach((token) => tokens.add(token));
hasStablePropagationToken = true;
}
if (typeof metadata.propagation_seed_key === 'string' && metadata.propagation_seed_key.length > 0) {
tokens.add(`seed-key:${metadata.propagation_seed_key}`);
hasStablePropagationToken = true;
}
if (typeof metadata.propagation_seed_signature === 'string' && metadata.propagation_seed_signature.length > 0) {
tokens.add(`seed-signature:${metadata.propagation_seed_signature}`);
hasStablePropagationToken = true;
}
if (isPropagatedMask(mask) && !hasStablePropagationToken) {
const source = typeof metadata.source === 'string' ? metadata.source : '';
const classKey = mask.classId || mask.className || '';
tokens.add([
'legacy-propagation',
source,
metadata.propagated_from_frame_id ?? '',
metadata.propagated_from_frame_index ?? '',
metadata.propagation_direction ?? '',
classKey,
mask.label || '',
mask.color || '',
].join(':'));
}
return tokens;
};