完善项目导入、模板与分割工作区交互
- 增强 DICOM/视频项目导入与演示数据:DICOM 按文件名自然顺序处理,导入后展示上传与解析任务进度,恢复演示出厂设置保留演示视频和演示 DICOM 项目,并补充 demo media seed 逻辑。 - 完善项目管理:项目支持重命名、删除、复制,删除使用站内确认弹窗,复制支持新项目重置和全内容复制,DICOM 项目不显示生成帧入口。 - 完善 GT Mask 与导出链路:只支持 8-bit maskid 图导入,非法/全背景图明确拒绝,尺寸自动适配,高精度 polygon 回显;统一导出默认当前帧,GT_label 使用 uint8 和真实 maskid,待分类 maskid 0 与背景一致。 - 完善分割工作区交互:新增画笔和橡皮擦并支持尺寸控制,移除创建点/线段入口,工具栏按类别分隔,AI 智能分割使用明确 AI 图标,取消黄色 seed point,清空/删除传播 mask 后同步清理空帧时间轴状态。 - 完善传播与时间轴:自动传播使用 SAM 2.1 权重任务,参考帧无遮罩时提示,传播历史按同一蓝色系递进变暗,删除/清空传播链时保留人工或独立 AI 标注来源。 - 完善模板库:新增头颈部 CT 分割默认模板,所有模板保留 maskid 0 待分类,支持鼠标复制模板、拖拽层级、JSON 批量导入预览、删除 label 和站内删除确认。 - 完善用户与高风险确认:用户改密码、删除用户、恢复演示出厂设置和清空人工/AI 标注帧均改为站内确认交互,避免浏览器原生 prompt/confirm。 - 补充前后端测试与文档:更新项目、模板、GT 导入、导出、传播、DICOM、用户管理等测试,并同步 README、AGENTS 和 doc 下实现/契约/测试计划文档。
This commit is contained in:
@@ -466,7 +466,7 @@ describe('CanvasArea', () => {
|
||||
expect(screen.getByText('当前图层: 胆囊 #21')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders imported GT seed points for editable point regions', () => {
|
||||
it('does not render stored GT seed points as visible editable handles', () => {
|
||||
useStore.setState({
|
||||
masks: [
|
||||
{
|
||||
@@ -482,7 +482,28 @@ describe('CanvasArea', () => {
|
||||
|
||||
render(<CanvasArea activeTool="move" frame={frame} />);
|
||||
|
||||
expect(screen.getAllByTestId('konva-circle')).toHaveLength(2);
|
||||
expect(screen.queryAllByTestId('konva-circle')
|
||||
.filter((element) => element.getAttribute('data-fill') === '#facc15')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not derive visible seed points for ordinary polygon masks', () => {
|
||||
useStore.setState({
|
||||
masks: [
|
||||
{
|
||||
id: 'manual-1',
|
||||
frameId: 'frame-1',
|
||||
pathData: 'M 10 10 L 90 10 L 90 40 Z',
|
||||
label: 'Manual',
|
||||
color: '#06b6d4',
|
||||
segmentation: [[10, 10, 90, 10, 90, 40]],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<CanvasArea activeTool="move" frame={frame} />);
|
||||
|
||||
expect(screen.queryAllByTestId('konva-circle')
|
||||
.filter((element) => element.getAttribute('data-fill') === '#facc15')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('selects a polygon mask and drags a vertex into dirty saved state', () => {
|
||||
@@ -668,6 +689,79 @@ describe('CanvasArea', () => {
|
||||
expect(onDeleteMaskAnnotations).toHaveBeenCalledWith(['99']);
|
||||
});
|
||||
|
||||
it('deletes linked propagated masks while keeping independent AI inference masks', () => {
|
||||
const onDeleteMaskAnnotations = vi.fn();
|
||||
const propagatedFrame = { ...frame, id: 'frame-2', index: 1, url: '/frame-2.jpg' };
|
||||
useStore.setState({
|
||||
masks: [
|
||||
{
|
||||
id: 'annotation-99',
|
||||
annotationId: '99',
|
||||
frameId: 'frame-1',
|
||||
pathData: 'M 10 10 L 90 10 L 90 40 Z',
|
||||
label: 'Seed',
|
||||
color: '#06b6d4',
|
||||
saveStatus: 'saved',
|
||||
saved: true,
|
||||
segmentation: [[10, 10, 90, 10, 90, 40]],
|
||||
},
|
||||
{
|
||||
id: 'annotation-100',
|
||||
annotationId: '100',
|
||||
frameId: 'frame-2',
|
||||
pathData: 'M 12 10 L 92 10 L 92 40 Z',
|
||||
label: 'Propagated A',
|
||||
color: '#06b6d4',
|
||||
saveStatus: 'saved',
|
||||
saved: true,
|
||||
segmentation: [[12, 10, 92, 10, 92, 40]],
|
||||
metadata: {
|
||||
source: 'sam2.1_hiera_tiny_propagation',
|
||||
source_annotation_id: 99,
|
||||
source_mask_id: 'annotation-99',
|
||||
propagation_seed_key: 'annotation:99',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'annotation-101',
|
||||
annotationId: '101',
|
||||
frameId: 'frame-3',
|
||||
pathData: 'M 14 10 L 94 10 L 94 40 Z',
|
||||
label: 'Propagated B',
|
||||
color: '#06b6d4',
|
||||
saveStatus: 'saved',
|
||||
saved: true,
|
||||
segmentation: [[14, 10, 94, 10, 94, 40]],
|
||||
metadata: {
|
||||
source: 'sam2.1_hiera_tiny_propagation',
|
||||
source_annotation_id: 99,
|
||||
source_mask_id: 'annotation-99',
|
||||
propagation_seed_key: 'annotation:99',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'annotation-102',
|
||||
annotationId: '102',
|
||||
frameId: 'frame-3',
|
||||
pathData: 'M 200 10 L 260 10 L 260 40 Z',
|
||||
label: 'AI Candidate',
|
||||
color: '#22c55e',
|
||||
saveStatus: 'saved',
|
||||
saved: true,
|
||||
segmentation: [[200, 10, 260, 10, 260, 40]],
|
||||
metadata: { source: 'ai_segmentation' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<CanvasArea activeTool="move" frame={propagatedFrame} onDeleteMaskAnnotations={onDeleteMaskAnnotations} />);
|
||||
fireEvent.click(screen.getByTestId('konva-path'));
|
||||
fireEvent.keyDown(window, { key: 'Delete' });
|
||||
|
||||
expect(useStore.getState().masks.map((mask) => mask.id)).toEqual(['annotation-99', 'annotation-102']);
|
||||
expect(onDeleteMaskAnnotations).toHaveBeenCalledWith(['100', '101']);
|
||||
});
|
||||
|
||||
it('inserts a polygon vertex from an edge midpoint handle', () => {
|
||||
useStore.setState({
|
||||
masks: [
|
||||
@@ -784,7 +878,8 @@ describe('CanvasArea', () => {
|
||||
const paths = screen.getAllByTestId('konva-path');
|
||||
fireEvent.click(paths[0]);
|
||||
expect(screen.getByText('已选 1')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('konva-circle')).toHaveLength(0);
|
||||
expect(screen.queryAllByTestId('konva-circle')
|
||||
.filter((element) => element.getAttribute('data-fill') === '#ffffff')).toHaveLength(0);
|
||||
fireEvent.click(paths[1]);
|
||||
expect(screen.getByText('已选 2')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '合并选中' }));
|
||||
@@ -1069,7 +1164,8 @@ describe('CanvasArea', () => {
|
||||
shape: '多边形',
|
||||
}),
|
||||
}));
|
||||
expect(screen.queryAllByTestId('konva-circle')).toHaveLength(0);
|
||||
expect(screen.queryAllByTestId('konva-circle')
|
||||
.filter((element) => element.getAttribute('data-fill') === '#facc15')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('shows contextual guidance for boolean selection ordering', () => {
|
||||
|
||||
Reference in New Issue
Block a user