2026-05-07-18-42-53 优化可视化工具栏和构件ID联动
This commit is contained in:
@@ -10,6 +10,13 @@ type ProjectStatus = 'pending' | 'completed' | 'processing';
|
||||
type DicomPlane = 'axial' | 'sagittal' | 'coronal';
|
||||
type DicomDisplayMode = 'default' | 'bone' | 'soft' | 'contrast';
|
||||
|
||||
interface ModuleStyleRecord {
|
||||
visible: boolean;
|
||||
color: string;
|
||||
opacity: number;
|
||||
partId: number;
|
||||
}
|
||||
|
||||
interface UserRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -33,6 +40,7 @@ interface ProjectRecord {
|
||||
maskFormats: Array<'nii' | 'nii.gz'>;
|
||||
exportedMaskCount: number;
|
||||
isDefault?: boolean;
|
||||
moduleStyles: Record<string, ModuleStyleRecord>;
|
||||
}
|
||||
|
||||
interface SessionRecord {
|
||||
@@ -70,6 +78,7 @@ const dicomVolumeCache = new Map<DicomDisplayMode, {
|
||||
spacingBetweenSlices: number | null;
|
||||
}>();
|
||||
const modelPreviewCache = new Map<string, unknown>();
|
||||
const defaultModuleColors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#14b8a6', '#f97316', '#64748b', '#ec4899'];
|
||||
|
||||
interface DicomAttributes {
|
||||
patientName: string;
|
||||
@@ -146,6 +155,37 @@ function publicSession(state: AppState) {
|
||||
};
|
||||
}
|
||||
|
||||
function clampNumber(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function normalizeModuleStyle(
|
||||
style: Partial<ModuleStyleRecord> | undefined,
|
||||
index: number,
|
||||
): ModuleStyleRecord {
|
||||
const opacity = typeof style?.opacity === 'number' && Number.isFinite(style.opacity) ? style.opacity : 0.72;
|
||||
const partId = typeof style?.partId === 'number' && Number.isFinite(style.partId) ? style.partId : index + 1;
|
||||
|
||||
return {
|
||||
visible: typeof style?.visible === 'boolean' ? style.visible : true,
|
||||
color: typeof style?.color === 'string' && /^#[0-9a-fA-F]{6}$/.test(style.color)
|
||||
? style.color
|
||||
: defaultModuleColors[index % defaultModuleColors.length],
|
||||
opacity: clampNumber(opacity, 0.1, 1),
|
||||
partId: clampNumber(Math.round(partId), 1, 255),
|
||||
};
|
||||
}
|
||||
|
||||
function buildModuleStyles(
|
||||
stlFiles: string[],
|
||||
existing?: Record<string, Partial<ModuleStyleRecord>>,
|
||||
) {
|
||||
return stlFiles.reduce<Record<string, ModuleStyleRecord>>((acc, fileName, index) => {
|
||||
acc[fileName] = normalizeModuleStyle(existing?.[fileName], index);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function buildDefaultProject(): ProjectRecord {
|
||||
const stlFiles = listFiles(modelDir, '.stl');
|
||||
|
||||
@@ -163,6 +203,7 @@ function buildDefaultProject(): ProjectRecord {
|
||||
maskFormats: ['nii', 'nii.gz'],
|
||||
exportedMaskCount: 0,
|
||||
isDefault: true,
|
||||
moduleStyles: buildModuleStyles(stlFiles),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,6 +221,7 @@ function buildEmptyProject(name: string): ProjectRecord {
|
||||
stlFiles: [],
|
||||
maskFormats: ['nii', 'nii.gz'],
|
||||
exportedMaskCount: 0,
|
||||
moduleStyles: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,13 +239,16 @@ function defaultState(): AppState {
|
||||
|
||||
function normalizeState(state: AppState): AppState {
|
||||
const defaultProject = buildDefaultProject();
|
||||
const savedDefaultProject = state.projects?.find((project) => project.id === defaultProject.id);
|
||||
const customProjects = Array.isArray(state.projects)
|
||||
? state.projects
|
||||
.filter((project) => project.id !== defaultProject.id)
|
||||
.map((project) => ({
|
||||
...project,
|
||||
stlFiles: Array.isArray(project.stlFiles) ? project.stlFiles : [],
|
||||
exportedMaskCount: project.exportedMaskCount ?? 0,
|
||||
maskFormats: project.maskFormats ?? ['nii', 'nii.gz'],
|
||||
moduleStyles: buildModuleStyles(Array.isArray(project.stlFiles) ? project.stlFiles : [], project.moduleStyles),
|
||||
}))
|
||||
: [];
|
||||
|
||||
@@ -212,8 +257,9 @@ function normalizeState(state: AppState): AppState {
|
||||
projects: [
|
||||
{
|
||||
...defaultProject,
|
||||
name: state.projects?.find((project) => project.id === defaultProject.id)?.name ?? defaultProject.name,
|
||||
exportedMaskCount: state.projects?.find((project) => project.id === defaultProject.id)?.exportedMaskCount ?? 0,
|
||||
name: savedDefaultProject?.name ?? defaultProject.name,
|
||||
exportedMaskCount: savedDefaultProject?.exportedMaskCount ?? 0,
|
||||
moduleStyles: buildModuleStyles(defaultProject.stlFiles, savedDefaultProject?.moduleStyles),
|
||||
},
|
||||
...customProjects,
|
||||
],
|
||||
@@ -1052,6 +1098,28 @@ async function startServer() {
|
||||
res.json({ ok: true, deletedId: deleted.id });
|
||||
});
|
||||
|
||||
app.patch('/api/projects/:projectId/module-styles', (req, res) => {
|
||||
const incoming = req.body?.moduleStyles;
|
||||
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) {
|
||||
res.status(400).json({ message: '构件样式数据无效' });
|
||||
return;
|
||||
}
|
||||
|
||||
const state = readState();
|
||||
const project = findProject(state, req.params.projectId);
|
||||
if (!project) {
|
||||
res.status(404).json({ message: '项目不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
project.moduleStyles = buildModuleStyles(project.stlFiles, {
|
||||
...(project.moduleStyles ?? {}),
|
||||
...(incoming as Record<string, Partial<ModuleStyleRecord>>),
|
||||
});
|
||||
writeState(state);
|
||||
res.json(project);
|
||||
});
|
||||
|
||||
app.get('/api/projects/:projectId/dicom-preview', (req, res) => {
|
||||
const project = findProject(readState(), req.params.projectId);
|
||||
if (!project) {
|
||||
|
||||
Reference in New Issue
Block a user