取消画笔自动合并选中遮罩

- 移除画笔与选中 mask 连通或重叠时自动 union 到旧 mask 的逻辑,画笔每次松手都生成新的独立 mask。

- 更新画笔工具提示,明确画笔会生成新的独立 mask。

- 调整 CanvasArea 回归测试,验证画笔触碰选中 mask 时仍保留旧 mask 并新建独立 mask。

- 更新 README、AGENTS、前端审计、需求冻结和测试计划文档,移除画笔自动合并描述。
This commit is contained in:
2026-05-04 03:51:22 +08:00
parent b7de163054
commit b1bb792b71
7 changed files with 21 additions and 45 deletions

View File

@@ -1723,10 +1723,10 @@ describe('CanvasArea', () => {
}
});
it('merges a connected brush stroke into the selected mask', () => {
it('creates an independent brush mask even when it touches the selected mask', () => {
useStore.setState({
activeTemplateId: '2',
activeClass: { id: 'c1', name: '胆囊', color: '#ff0000', zIndex: 20 },
activeClass: { id: 'c1', name: '胆囊', color: '#ff0000', zIndex: 20, maskId: 1 },
activeClassId: 'c1',
selectedMaskIds: ['m1'],
masks: [
@@ -1749,15 +1749,23 @@ describe('CanvasArea', () => {
fireEvent.mouseMove(stage, { clientX: 170, clientY: 100 });
fireEvent.mouseUp(stage, { clientX: 210, clientY: 110 });
expect(useStore.getState().masks).toHaveLength(1);
expect(useStore.getState().masks).toHaveLength(2);
expect(useStore.getState().masks[0]).toEqual(expect.objectContaining({
id: 'm1',
label: '胆囊',
color: '#ff0000',
saveStatus: 'draft',
area: 2500,
}));
expect(useStore.getState().masks[0].area).toBeGreaterThan(2500);
expect(useStore.getState().selectedMaskIds).toEqual(['m1']);
expect(useStore.getState().masks[1]).toEqual(expect.objectContaining({
label: '胆囊',
color: '#ff0000',
classId: 'c1',
classMaskId: 1,
saveStatus: 'draft',
metadata: expect.objectContaining({ shape: '画笔' }),
}));
expect(useStore.getState().masks[1].area).toBeGreaterThan(1000);
expect(useStore.getState().selectedMaskIds).toEqual([useStore.getState().masks[1].id]);
});
it('erases from the selected mask with a sampled stroke', () => {

View File

@@ -512,10 +512,6 @@ function imageBoundsGeometry(width: number, height: number): MultiPolygon | null
]]);
}
function geometriesOverlap(first: MultiPolygon, second: MultiPolygon): boolean {
return polygonClipping.intersection(first, second).length > 0;
}
export function CanvasArea({
activeTool,
frame,
@@ -643,7 +639,7 @@ export function CanvasArea({
return {
title: '画笔',
body: activeClass
? '按住并拖动画出连续区域;若与当前选中 mask 连通,会自动合并到该 mask。'
? '按住并拖动画出连续区域,松开后生成一个新的独立 mask。'
: '先在右侧语义分类树选择类别,然后按住并拖动画出连续区域。',
};
}
@@ -1150,34 +1146,6 @@ export function CanvasArea({
return;
}
const targetGeometry = selectedMask ? maskToMultiPolygon(selectedMask) : null;
const shouldMerge = Boolean(targetGeometry && geometriesOverlap(targetGeometry, strokeGeometry));
if (selectedMask && targetGeometry && shouldMerge) {
const resultGeometry = polygonClipping.union(targetGeometry, strokeGeometry);
const resultSegmentation = multiPolygonToSegmentation(resultGeometry);
if (resultSegmentation.length === 0) return;
const nextMask = {
...maskWithSegmentation(selectedMask, resultSegmentation, {
area: multiPolygonArea(resultGeometry),
hasHoles: multiPolygonHasHoles(resultGeometry),
polygonRingCounts: multiPolygonRingCounts(resultGeometry),
}),
templateId: activeTemplateId || selectedMask.templateId,
classId: activeClass.id,
className: activeClass.name,
classZIndex: activeClass.zIndex,
classMaskId: activeClass.maskId,
label: activeClass.name,
color: activeClass.color,
};
setMasks(masks.map((mask) => (mask.id === selectedMask.id ? nextMask : mask)));
setSelectedMaskId(selectedMask.id);
setSelectedMaskIds([selectedMask.id]);
setGlobalSelectedMaskIds([selectedMask.id]);
setSelectedVertexIndex(null);
return;
}
const nextMask = createManualMaskFromGeometry('画笔', strokeGeometry);
if (nextMask) {
setSelectedMaskId(nextMask.id);