2026-05-24-15-55-48 增加项目锁定与切片控件修正
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Eye,
|
||||
Lock,
|
||||
Maximize2,
|
||||
RefreshCcw,
|
||||
Save,
|
||||
@@ -2283,8 +2284,7 @@ export function VoxelizationMappingView({
|
||||
const stepSlice = (delta: number) => {
|
||||
onSliceChange(clamp(safeSlice + delta, 0, maxSlice));
|
||||
};
|
||||
const sliderSliceValue = maxSlice - safeSlice;
|
||||
const slicePercent = maxSlice > 0 ? (sliderSliceValue / maxSlice) * 100 : 0;
|
||||
const sliderSliceValue = safeSlice;
|
||||
const displaySliceNumber = getDicomDisplaySliceNumber(safeSlice, Math.max(totalSlices, 1));
|
||||
const resetMappingViewport = () => {
|
||||
setMappingViewport({ scale: 1, offsetX: 0, offsetY: 0 });
|
||||
@@ -2423,16 +2423,12 @@ export function VoxelizationMappingView({
|
||||
<aside className="flex min-h-0 flex-col items-center gap-3 border-l border-white/10 bg-[#0f172a] px-2 py-5">
|
||||
<div className="relative min-h-[220px] w-8 flex-1">
|
||||
<div className="absolute inset-y-0 left-1/2 w-1.5 -translate-x-1/2 rounded-full bg-white/10" />
|
||||
<div
|
||||
className="absolute bottom-0 left-1/2 w-1.5 -translate-x-1/2 rounded-full bg-cyan-400"
|
||||
style={{ height: `${slicePercent}%` }}
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={maxSlice}
|
||||
value={sliderSliceValue}
|
||||
onChange={(event) => onSliceChange(maxSlice - Number(event.target.value))}
|
||||
onChange={(event) => onSliceChange(Number(event.target.value))}
|
||||
className="mapping-slice-dark-vertical-input"
|
||||
aria-label="项目库逆向分割映射视图切片导航"
|
||||
/>
|
||||
@@ -2533,8 +2529,8 @@ export function VoxelizationMappingView({
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => stepSlice(-1)}
|
||||
disabled={safeSlice <= 0}
|
||||
onClick={() => stepSlice(1)}
|
||||
disabled={safeSlice >= maxSlice}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 shadow-sm hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600 disabled:opacity-35"
|
||||
title="上一层"
|
||||
>
|
||||
@@ -2542,32 +2538,28 @@ export function VoxelizationMappingView({
|
||||
</button>
|
||||
<div className="relative min-h-[240px] w-10 flex-1">
|
||||
<div className="absolute inset-y-0 left-1/2 w-2 -translate-x-1/2 rounded-full bg-slate-200" />
|
||||
<div
|
||||
className="absolute bottom-0 left-1/2 w-2 -translate-x-1/2 rounded-full bg-blue-600"
|
||||
style={{ height: `${slicePercent}%` }}
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={maxSlice}
|
||||
value={sliderSliceValue}
|
||||
onChange={(event) => onSliceChange(maxSlice - Number(event.target.value))}
|
||||
onChange={(event) => onSliceChange(Number(event.target.value))}
|
||||
className="mapping-slice-vertical-input"
|
||||
aria-label="逆向分割映射视图切片导航"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => stepSlice(1)}
|
||||
disabled={safeSlice >= maxSlice}
|
||||
onClick={() => stepSlice(-1)}
|
||||
disabled={safeSlice <= 0}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 shadow-sm hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600 disabled:opacity-35"
|
||||
title="下一层"
|
||||
>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
<div className="grid w-full grid-cols-1 gap-1 text-center text-[9px] font-bold text-slate-500">
|
||||
<span>顶层 {Math.max(totalSlices, 1)}</span>
|
||||
<span>顶层 1</span>
|
||||
<span className="text-blue-600">当前 {displaySliceNumber}</span>
|
||||
<span>底层 1</span>
|
||||
<span>底层 {Math.max(totalSlices, 1)}</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -2759,6 +2751,9 @@ export default function ReverseWorkspace({
|
||||
if (!project) {
|
||||
return true;
|
||||
}
|
||||
if (project.locked) {
|
||||
return true;
|
||||
}
|
||||
if (savedWorkspaceSnapshotRef.current === getCurrentWorkspaceSnapshot()) {
|
||||
return true;
|
||||
}
|
||||
@@ -2918,6 +2913,19 @@ export default function ReverseWorkspace({
|
||||
});
|
||||
api.getProject(projectId).then((item) => {
|
||||
setProject(item);
|
||||
if (item.locked) {
|
||||
setFusionVolume(null);
|
||||
setWorkspaceLoadState({
|
||||
ready: false,
|
||||
phase: '项目已锁定',
|
||||
loaded: 1,
|
||||
total: 1,
|
||||
startedAt: Date.now(),
|
||||
error: '项目已锁定,请在项目库解锁后再进入逆向工作区。',
|
||||
});
|
||||
savedWorkspaceSnapshotRef.current = '';
|
||||
return;
|
||||
}
|
||||
const maxIndex = Math.max((item.dicomCount || 1) - 1, 0);
|
||||
const latestResult = item.segmentationResults?.[item.segmentationResults.length - 1];
|
||||
const restoredSliceStart = clamp(latestResult?.sliceStart ?? 0, 0, maxIndex);
|
||||
@@ -3167,6 +3175,21 @@ export default function ReverseWorkspace({
|
||||
restoreVisualToolbarScroll(scrollTop);
|
||||
};
|
||||
|
||||
const allModulesVisible = Boolean(project?.stlFiles?.length) && (project?.stlFiles ?? []).every((fileName) => moduleStyles[fileName]?.visible !== false);
|
||||
|
||||
const toggleAllModules = () => {
|
||||
const stlFiles = project?.stlFiles ?? [];
|
||||
const nextVisible = !allModulesVisible;
|
||||
const next = { ...moduleStyles };
|
||||
stlFiles.forEach((fileName, index) => {
|
||||
next[fileName] = makeDefaultModuleStyle(index, {
|
||||
...(next[fileName] ?? project?.moduleStyles?.[fileName]),
|
||||
visible: nextVisible,
|
||||
});
|
||||
});
|
||||
commitModuleStyles(next);
|
||||
};
|
||||
|
||||
const updateModuleStyle = (fileName: string, partial: Partial<ModuleStyle>) => {
|
||||
const stlFiles = project?.stlFiles ?? [];
|
||||
const index = Math.max(0, stlFiles.indexOf(fileName));
|
||||
@@ -3280,7 +3303,7 @@ export default function ReverseWorkspace({
|
||||
const workspaceLoadSpeed = workspaceLoadState.loaded / workspaceElapsedSeconds;
|
||||
|
||||
useEffect(() => {
|
||||
if (!project?.dicomCount) {
|
||||
if (!project?.dicomCount || project.locked) {
|
||||
return undefined;
|
||||
}
|
||||
if (workspaceLoadProjectRef.current === project.id) {
|
||||
@@ -3371,6 +3394,7 @@ export default function ReverseWorkspace({
|
||||
}, [
|
||||
project?.id,
|
||||
project?.dicomCount,
|
||||
project?.locked,
|
||||
project?.stlFiles?.join('|'),
|
||||
displayStart,
|
||||
displayEnd,
|
||||
@@ -3398,6 +3422,27 @@ export default function ReverseWorkspace({
|
||||
modelPose.rotateZ,
|
||||
]);
|
||||
|
||||
if (project?.locked) {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 items-center justify-center overflow-hidden pr-2">
|
||||
<div className="w-full max-w-2xl rounded-3xl border border-amber-100 bg-white p-8 text-center shadow-sm">
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-amber-50 text-amber-600">
|
||||
<Lock size={24} />
|
||||
</div>
|
||||
<h2 className="mt-5 text-2xl font-black text-slate-900">项目已锁定</h2>
|
||||
<p className="mx-auto mt-3 max-w-lg text-sm font-semibold leading-6 text-slate-500">
|
||||
请先在项目库点击“解锁项目”,再进入逆向工作区继续修改位姿和分割结果。
|
||||
</p>
|
||||
<div className="mt-6 rounded-2xl bg-slate-50 px-4 py-3 text-left text-xs font-bold text-slate-500">
|
||||
<p>项目:{project.name}</p>
|
||||
<p className="mt-1">锁定时间:{project.lockedAt ? new Date(project.lockedAt).toLocaleString('zh-CN') : '未记录'}</p>
|
||||
<p className="mt-1">锁定结果:{project.lockedPoseSnapshotPath ?? '项目数据/锁定结果'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspaceLoadState.ready) {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 items-center justify-center overflow-hidden pr-2">
|
||||
@@ -3943,7 +3988,17 @@ export default function ReverseWorkspace({
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">构件层级</p>
|
||||
<span className="text-[10px] font-mono text-slate-400">{project?.stlFiles?.length ?? 0}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-mono text-slate-400">{project?.stlFiles?.length ?? 0}</span>
|
||||
<button
|
||||
onClick={toggleAllModules}
|
||||
disabled={!project?.stlFiles?.length}
|
||||
className={`rounded p-1 transition ${allModulesVisible ? 'text-blue-500' : 'text-slate-300'} hover:bg-white disabled:opacity-40`}
|
||||
title={allModulesVisible ? '隐藏所有构件' : '显示所有构件'}
|
||||
>
|
||||
<Eye size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{(project?.stlFiles ?? []).map((fileName, index) => {
|
||||
|
||||
Reference in New Issue
Block a user