Align DICOM UPP registration uniqueness
This commit is contained in:
@@ -12,6 +12,7 @@ const state = {
|
||||
stlFiles: [],
|
||||
selectedStlIds: new Set(),
|
||||
selectedFamilies: new Set(),
|
||||
selectedAlgorithmModel: "",
|
||||
mode: "file",
|
||||
registrations: [],
|
||||
pose: defaultPose(),
|
||||
@@ -335,15 +336,46 @@ async function selectCase(ctKey) {
|
||||
function chooseDefaultStl() {
|
||||
state.selectedStlIds.clear();
|
||||
state.selectedFamilies.clear();
|
||||
if (!state.stlFiles.length) return;
|
||||
const families = Array.from(new Set(state.stlFiles.map((item) => item.family).filter(Boolean)));
|
||||
if (!state.stlFiles.length) {
|
||||
state.selectedAlgorithmModel = "";
|
||||
return;
|
||||
}
|
||||
const models = algorithmModels();
|
||||
const nextModel = models.includes(state.selectedAlgorithmModel) ? state.selectedAlgorithmModel : models[0] || "";
|
||||
selectDefaultStlForModel(nextModel);
|
||||
}
|
||||
|
||||
function algorithmModels() {
|
||||
return Array.from(new Set(state.stlFiles.map((item) => item.algorithm_model || "未指定模型").filter(Boolean))).sort();
|
||||
}
|
||||
|
||||
function currentAlgorithmModel() {
|
||||
const selected = selectedStlFiles();
|
||||
const selectedModel = selected.find((item) => item.algorithm_model)?.algorithm_model;
|
||||
if (selectedModel) return selectedModel;
|
||||
if (state.selectedAlgorithmModel) return state.selectedAlgorithmModel;
|
||||
return algorithmModels()[0] || state.activeCase?.algorithm_model || "未指定模型";
|
||||
}
|
||||
|
||||
function stlFilesForSelectedModel() {
|
||||
const model = currentAlgorithmModel();
|
||||
return model ? state.stlFiles.filter((item) => (item.algorithm_model || "未指定模型") === model) : state.stlFiles;
|
||||
}
|
||||
|
||||
function selectDefaultStlForModel(model) {
|
||||
state.selectedStlIds.clear();
|
||||
state.selectedFamilies.clear();
|
||||
state.selectedAlgorithmModel = model || algorithmModels()[0] || "";
|
||||
const candidates = stlFilesForSelectedModel();
|
||||
if (!candidates.length) return;
|
||||
const families = Array.from(new Set(candidates.map((item) => item.family).filter(Boolean)));
|
||||
const preferred = ["liver", "portal_vein", "liver_artery", "liver_vein", "bile_duct", "pancreas", "spleen"];
|
||||
const selectedFamily = preferred.find((family) => families.includes(family)) || families[0];
|
||||
if (selectedFamily) {
|
||||
state.selectedFamilies.add(selectedFamily);
|
||||
state.stlFiles.filter((item) => item.family === selectedFamily).forEach((item) => state.selectedStlIds.add(Number(item.id)));
|
||||
candidates.filter((item) => item.family === selectedFamily).forEach((item) => state.selectedStlIds.add(Number(item.id)));
|
||||
} else {
|
||||
state.selectedStlIds.add(Number(state.stlFiles[0].id));
|
||||
state.selectedStlIds.add(Number(candidates[0].id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,24 +455,40 @@ function renderStl() {
|
||||
$("stlCount").textContent = String(state.stlFiles.length);
|
||||
$("modeFile").classList.toggle("active", state.mode === "file");
|
||||
$("modeFamily").classList.toggle("active", state.mode === "family");
|
||||
const families = Array.from(new Set(state.stlFiles.map((item) => item.family).filter(Boolean)));
|
||||
const models = algorithmModels();
|
||||
if (models.length && !models.includes(state.selectedAlgorithmModel)) {
|
||||
state.selectedAlgorithmModel = models[0];
|
||||
}
|
||||
$("modelRail").innerHTML = models
|
||||
.map((model) => `<button class="chip ${model === currentAlgorithmModel() ? "active" : ""}" data-model="${escapeHtml(model)}" type="button">${escapeHtml(model)}</button>`)
|
||||
.join("");
|
||||
const visibleFiles = stlFilesForSelectedModel();
|
||||
const families = Array.from(new Set(visibleFiles.map((item) => item.family).filter(Boolean)));
|
||||
$("familyRail").innerHTML = families
|
||||
.map((family) => `<button class="chip ${state.selectedFamilies.has(family) ? "active" : ""}" data-family="${escapeHtml(family)}" type="button">${escapeHtml(family)}</button>`)
|
||||
.join("");
|
||||
document.querySelectorAll("[data-model]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
selectDefaultStlForModel(button.dataset.model);
|
||||
markUnsaved();
|
||||
renderStl();
|
||||
await reloadStlVisuals();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-family]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const family = button.dataset.family;
|
||||
if (state.selectedFamilies.has(family)) state.selectedFamilies.delete(family);
|
||||
else state.selectedFamilies.add(family);
|
||||
state.selectedStlIds.clear();
|
||||
state.stlFiles.filter((item) => state.selectedFamilies.has(item.family)).forEach((item) => state.selectedStlIds.add(Number(item.id)));
|
||||
visibleFiles.filter((item) => state.selectedFamilies.has(item.family)).forEach((item) => state.selectedStlIds.add(Number(item.id)));
|
||||
markUnsaved();
|
||||
renderStl();
|
||||
await reloadStlVisuals();
|
||||
});
|
||||
});
|
||||
$("stlList").innerHTML = state.stlFiles.length
|
||||
? state.stlFiles
|
||||
$("stlList").innerHTML = visibleFiles.length
|
||||
? visibleFiles
|
||||
.map((row) => {
|
||||
const checked = state.selectedStlIds.has(Number(row.id));
|
||||
return `
|
||||
@@ -533,9 +581,11 @@ async function reloadStlVisuals() {
|
||||
|
||||
function applyLatestRegistration() {
|
||||
const seriesUid = state.selectedSeries?.series_uid || "";
|
||||
const selectedLabel = currentStlLabel();
|
||||
const exact = state.registrations.find((item) => item.series_instance_uid === seriesUid && item.stl_family === selectedLabel);
|
||||
const latest = exact || state.registrations.find((item) => item.series_instance_uid === seriesUid) || null;
|
||||
const model = currentAlgorithmModel();
|
||||
const latest =
|
||||
state.registrations.find((item) => item.algorithm_model === model) ||
|
||||
state.registrations.find((item) => item.series_instance_uid === seriesUid) ||
|
||||
null;
|
||||
if (latest?.transform) {
|
||||
state.pose = { ...defaultPose(), ...latest.transform };
|
||||
$("registrationNotes").value = latest.notes || "";
|
||||
@@ -557,11 +607,16 @@ async function saveRegistration(locked = false, forceUnlock = false) {
|
||||
alert("请至少选择一个 STL 构件");
|
||||
return;
|
||||
}
|
||||
const selectedModels = Array.from(new Set(selected.map((item) => item.algorithm_model || "未指定模型")));
|
||||
if (selectedModels.length > 1) {
|
||||
alert("同一次配准只能选择同一个算法模型的 STL");
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
ct_number: state.activeCase.pacs_ct_number || state.activeCase.ct_key,
|
||||
series_instance_uid: state.selectedSeries.series_uid,
|
||||
series_description: state.selectedSeries.description || "",
|
||||
algorithm_model: state.activeCase.algorithm_model || selected[0]?.algorithm_model || "未指定模型",
|
||||
algorithm_model: selectedModels[0] || currentAlgorithmModel(),
|
||||
stl_family: currentStlLabel(),
|
||||
view_mode: state.mode,
|
||||
selected_stl_files: selected.map((item) => ({
|
||||
@@ -584,7 +639,7 @@ async function saveRegistration(locked = false, forceUnlock = false) {
|
||||
slice_thickness: state.selectedSeries.slice_thickness,
|
||||
spacing_between_slices: state.selectedSeries.spacing_between_slices,
|
||||
},
|
||||
model_reference: { selected_count: selected.length, families: Array.from(state.selectedFamilies) },
|
||||
model_reference: { algorithm_model: selectedModels[0] || currentAlgorithmModel(), selected_count: selected.length, families: Array.from(state.selectedFamilies) },
|
||||
segmentation: { preview: state.maskVisible, source: "visible_stl_preview" },
|
||||
notes: $("registrationNotes").value.trim(),
|
||||
locked,
|
||||
|
||||
Reference in New Issue
Block a user