统一工作区清空遮罩入口

- 移除 Canvas 右下角旧清空遮罩和应用分类按钮,清空入口统一到左侧工具栏

- 清空遮罩优先作用于当前帧选中 mask,无选中时作用于当前帧全部 mask

- 目标 mask 无传播链结果时直接清当前帧,有传播链结果时弹窗选择只清当前帧、清空传播所有帧或取消

- 保留布尔工具右下角合并/去除操作区,避免旧分类按钮误改整帧

- 更新 Canvas、工具栏、工作区测试,覆盖直接清空、传播链范围选择和取消路径

- 同步更新前端审计、需求冻结、设计冻结、测试计划和 AGENTS 说明
This commit is contained in:
2026-05-03 21:06:03 +08:00
parent 3e998b9d6b
commit a22af5f7c8
11 changed files with 263 additions and 169 deletions

View File

@@ -9,7 +9,6 @@ import type { Frame, Mask } from '../store/useStore';
interface CanvasAreaProps {
activeTool: string;
frame: Frame | null;
onClearMasks?: () => void;
onDeleteMaskAnnotations?: (annotationIds: string[]) => Promise<void> | void;
}
@@ -417,7 +416,7 @@ function geometriesOverlap(first: MultiPolygon, second: MultiPolygon): boolean {
return polygonClipping.intersection(first, second).length > 0;
}
export function CanvasArea({ activeTool, frame, onClearMasks, onDeleteMaskAnnotations }: CanvasAreaProps) {
export function CanvasArea({ activeTool, frame, onDeleteMaskAnnotations }: CanvasAreaProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [stageSize, setStageSize] = useState({ width: 800, height: 600 });
const [scale, setScale] = useState(1);
@@ -448,7 +447,6 @@ export function CanvasArea({ activeTool, frame, onClearMasks, onDeleteMaskAnnota
const masks = useStore((state) => state.masks);
const addMask = useStore((state) => state.addMask);
const updateMask = useStore((state) => state.updateMask);
const clearMasks = useStore((state) => state.clearMasks);
const setMasks = useStore((state) => state.setMasks);
const setGlobalSelectedMaskIds = useStore((state) => state.setSelectedMaskIds);
const maskPreviewOpacity = useStore((state) => state.maskPreviewOpacity);
@@ -971,37 +969,6 @@ export function CanvasArea({ activeTool, frame, onClearMasks, onDeleteMaskAnnota
}
}, [activeClass, activeTemplateId, addMask, aiModel, frame?.height, frame?.id, frame?.width, image?.height, image?.naturalHeight, image?.naturalWidth, image?.width, masks, samCandidateMaskId, setMasks, updateMask]);
const handleApplyActiveClass = () => {
if (!frame?.id || !activeClass) return;
const seedIds = selectedMaskIds.length > 0
? selectedMaskIds
: frameMasks.map((mask) => mask.id);
const targetIds = findPropagationChainMaskIds(seedIds, masks);
setMasks(masks.map((mask) => {
if (!targetIds.has(mask.id)) return mask;
return {
...mask,
templateId: activeTemplateId || mask.templateId,
classId: activeClass.id,
className: activeClass.name,
classZIndex: activeClass.zIndex,
classMaskId: activeClass.maskId,
label: activeClass.name,
color: activeClass.color,
saveStatus: mask.annotationId ? 'dirty' : 'draft',
saved: Boolean(mask.annotationId) ? false : mask.saved,
};
}));
};
const handleClearMasks = () => {
if (onClearMasks) {
onClearMasks();
return;
}
clearMasks();
};
const deleteMasksById = useCallback((maskIds: string[]) => {
if (maskIds.length === 0) return;
const idSet = expandedPropagationDeletionMaskIds(maskIds, masks);
@@ -1772,36 +1739,20 @@ export function CanvasArea({ activeTool, frame, onClearMasks, onDeleteMaskAnnota
<span>: {dirtyMaskCount}</span>
</div>
{frameMasks.length > 0 && (
{frameMasks.length > 0 && isBooleanTool && (
<div className="absolute bottom-4 right-4 flex gap-2">
{isBooleanTool && (
<div className="flex items-center gap-2">
<span className="text-xs bg-white/5 text-gray-300 border border-white/10 px-2.5 py-1.5 rounded">
{booleanSelectedMasks.length}
</span>
<button
onClick={handleBooleanOperation}
disabled={booleanSelectedMasks.length < 2}
className="text-xs bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-300 border border-emerald-500/20 px-3 py-1.5 rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-emerald-500/10"
>
{effectiveTool === 'area_merge' ? '合并选中' : '从主区域去除'}
</button>
</div>
)}
{activeClass && (
<div className="flex items-center gap-2">
<span className="text-xs bg-white/5 text-gray-300 border border-white/10 px-2.5 py-1.5 rounded">
{booleanSelectedMasks.length}
</span>
<button
onClick={handleApplyActiveClass}
className="text-xs bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-300 border border-cyan-500/20 px-3 py-1.5 rounded transition-colors"
onClick={handleBooleanOperation}
disabled={booleanSelectedMasks.length < 2}
className="text-xs bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-300 border border-emerald-500/20 px-3 py-1.5 rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-emerald-500/10"
>
{effectiveTool === 'area_merge' ? '合并选中' : '从主区域去除'}
</button>
)}
<button
onClick={handleClearMasks}
className="text-xs bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 px-3 py-1.5 rounded transition-colors"
>
</button>
</div>
</div>
)}
</div>