Group liver STL models by anatomy
This commit is contained in:
@@ -71,6 +71,46 @@ const STL_DISPLAY_LABELS = {
|
||||
solid: "实体显示",
|
||||
translucent: "半透明",
|
||||
};
|
||||
const LIVER_STL_GROUPS = [
|
||||
{ key: "liver_segments", label: "肝段", order: 10 },
|
||||
{ key: "liver_body", label: "肝脏", order: 20 },
|
||||
{ key: "liver_vessels", label: "肝血管", order: 30 },
|
||||
{ key: "liver_lesions", label: "肿瘤和囊肿", order: 40 },
|
||||
{ key: "body_surface", label: "骨骼皮肤", order: 50 },
|
||||
{ key: "other", label: "其他", order: 90 },
|
||||
];
|
||||
const LIVER_STL_GROUP_LOOKUP = Object.fromEntries(LIVER_STL_GROUPS.map((group) => [group.key, group]));
|
||||
const LIVER_STL_LABELS = {
|
||||
bile_duct: "胆管",
|
||||
cholecyst: "胆囊",
|
||||
gallbladder: "胆囊",
|
||||
hipbone: "髋骨",
|
||||
liver: "肝脏整体",
|
||||
liver_artery: "肝动脉",
|
||||
liver_left: "左肝叶",
|
||||
liver_right: "右肝叶",
|
||||
liver_vein: "肝静脉",
|
||||
pancreas: "胰腺",
|
||||
portal_vein: "门静脉",
|
||||
rib: "肋骨",
|
||||
ribs: "肋骨",
|
||||
sacrum: "骶骨",
|
||||
skin: "皮肤",
|
||||
spleen: "脾脏",
|
||||
sternum: "胸骨",
|
||||
vertebra: "椎骨",
|
||||
vertebrae: "椎骨",
|
||||
};
|
||||
const ROMAN_NUMERALS = {
|
||||
1: "I",
|
||||
2: "II",
|
||||
3: "III",
|
||||
4: "IV",
|
||||
5: "V",
|
||||
6: "VI",
|
||||
7: "VII",
|
||||
8: "VIII",
|
||||
};
|
||||
|
||||
const state = {
|
||||
token: localStorage.getItem("dicom_upp_registration_token") || "",
|
||||
@@ -760,6 +800,106 @@ function stlFamilyLabel(file) {
|
||||
return String(file.family || "未分类");
|
||||
}
|
||||
|
||||
function normalizedStlText(file) {
|
||||
return [
|
||||
file.family,
|
||||
file.segment_name,
|
||||
file.file_name,
|
||||
file.category,
|
||||
].map((value) => String(value || "").toLowerCase()).join(" ");
|
||||
}
|
||||
|
||||
function normalizedStlName(file) {
|
||||
return String(file.segment_name || file.family || file.file_name || "")
|
||||
.replace(/\.stl$/i, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function stlSegmentNumber(file) {
|
||||
const text = normalizedStlText(file);
|
||||
const match = text.match(/(?:segment[_\s-]*s?|肝段)\s*([0-9]+)/i) || text.match(/_s([0-9]+)/i);
|
||||
return match ? Number(match[1]) : 999;
|
||||
}
|
||||
|
||||
function lesionNumber(file) {
|
||||
const text = normalizedStlText(file);
|
||||
const match = text.match(/(?:^|[_\s-])(?:liver[_\s-]*)?(?:tumor|lesion|cyst)[_\s-]*([0-9]+)/i)
|
||||
|| text.match(/(?:肿瘤|囊肿)[_\s-]*([0-9]+)/i);
|
||||
return match ? Number(match[1]) : 99;
|
||||
}
|
||||
|
||||
function isTumorStl(file, category = String(file.category || "")) {
|
||||
const text = normalizedStlText(file);
|
||||
return /(?:^|[_\s-])(?:liver[_\s-]*)?(?:tumor|lesion)(?:[_\s-]|\d|$)/i.test(text) || category.includes("肿瘤");
|
||||
}
|
||||
|
||||
function isCystStl(file, category = String(file.category || "")) {
|
||||
const text = normalizedStlText(file);
|
||||
return /(?:^|[_\s-])(?:liver[_\s-]*)?cyst(?:[_\s-]|\d|$)/i.test(text) || category.includes("囊肿");
|
||||
}
|
||||
|
||||
function numberedLesionLabel(file, baseLabel) {
|
||||
const number = lesionNumber(file);
|
||||
return number < 99 ? `${baseLabel} ${number}` : baseLabel;
|
||||
}
|
||||
|
||||
function liverStlPresentation(file) {
|
||||
const text = normalizedStlText(file);
|
||||
const name = normalizedStlName(file);
|
||||
const category = String(file.category || "");
|
||||
let groupKey = "other";
|
||||
let itemOrder = 900;
|
||||
let label = LIVER_STL_LABELS[name] || file.segment_name || file.family || file.file_name;
|
||||
|
||||
if (text.includes("liver_segment") || category.includes("肝段")) {
|
||||
const segment = stlSegmentNumber(file);
|
||||
groupKey = "liver_segments";
|
||||
itemOrder = segment;
|
||||
label = `肝段 ${ROMAN_NUMERALS[segment] || segment}`;
|
||||
} else if (["liver", "liver_left", "liver_right"].includes(name) || category.includes("肝脏主体")) {
|
||||
groupKey = "liver_body";
|
||||
itemOrder = { liver: 1, liver_left: 2, liver_right: 3 }[name] || 99;
|
||||
} else if (["liver_artery", "liver_vein", "portal_vein"].includes(name)) {
|
||||
groupKey = "liver_vessels";
|
||||
itemOrder = { liver_artery: 1, liver_vein: 2, portal_vein: 3 }[name] || 99;
|
||||
} else if (isTumorStl(file, category) || isCystStl(file, category)) {
|
||||
const tumor = isTumorStl(file, category);
|
||||
groupKey = "liver_lesions";
|
||||
itemOrder = (tumor ? 1 : 20) + lesionNumber(file) / 100;
|
||||
label = tumor
|
||||
? numberedLesionLabel(file, "肝脏肿瘤")
|
||||
: numberedLesionLabel(file, "肝囊肿");
|
||||
} else if (category.includes("体表骨骼") || ["hipbone", "rib", "ribs", "sacrum", "skin", "sternum", "vertebra", "vertebrae"].includes(name)) {
|
||||
groupKey = "body_surface";
|
||||
itemOrder = { vertebrae: 1, vertebra: 1, rib: 2, ribs: 2, sternum: 3, sacrum: 4, hipbone: 5, skin: 6 }[name] || 99;
|
||||
} else {
|
||||
itemOrder = { bile_duct: 1, cholecyst: 2, gallbladder: 2, pancreas: 3, spleen: 4 }[name] || 99;
|
||||
}
|
||||
|
||||
const group = LIVER_STL_GROUP_LOOKUP[groupKey] || LIVER_STL_GROUP_LOOKUP.other;
|
||||
return {
|
||||
key: group.key,
|
||||
label: group.label,
|
||||
order: group.order,
|
||||
itemOrder,
|
||||
itemLabel: label,
|
||||
hierarchy: true,
|
||||
};
|
||||
}
|
||||
|
||||
function stlPresentation(file) {
|
||||
if (isLiverBiliaryModel(file.algorithm_model || state.algorithmModel)) return liverStlPresentation(file);
|
||||
const label = stlFamilyLabel(file);
|
||||
return {
|
||||
key: label,
|
||||
label,
|
||||
order: 100,
|
||||
itemOrder: 100,
|
||||
itemLabel: file.segment_name || file.file_name,
|
||||
hierarchy: false,
|
||||
};
|
||||
}
|
||||
|
||||
function stlFileIndex(file) {
|
||||
const id = normalizedStlId(file);
|
||||
const index = state.stlFiles.findIndex((item) => normalizedStlId(item) === id);
|
||||
@@ -947,45 +1087,60 @@ function renderStl() {
|
||||
}
|
||||
const grouped = new Map();
|
||||
state.stlFiles.forEach((file, index) => {
|
||||
const family = stlFamilyLabel(file);
|
||||
if (!grouped.has(family)) grouped.set(family, []);
|
||||
grouped.get(family).push({ file, index });
|
||||
const presentation = stlPresentation(file);
|
||||
if (!grouped.has(presentation.key)) {
|
||||
grouped.set(presentation.key, {
|
||||
key: presentation.key,
|
||||
label: presentation.label,
|
||||
order: presentation.order,
|
||||
hierarchy: presentation.hierarchy,
|
||||
rows: [],
|
||||
});
|
||||
}
|
||||
grouped.get(presentation.key).rows.push({ file, index, presentation });
|
||||
});
|
||||
$("stlList").innerHTML = [...grouped.entries()].map(([family, rows]) => {
|
||||
const visibleCount = rows.filter(({ file }) => stlMode(file) !== "hidden").length;
|
||||
const collapsed = state.collapsedStlFamilies.has(family);
|
||||
return `
|
||||
<section class="stl-family ${collapsed ? "collapsed" : ""}">
|
||||
<button class="stl-family-head" type="button" data-family-toggle="${escapeHtml(family)}" title="收起/展开 ${escapeHtml(family)}">
|
||||
<span>${escapeHtml(family)}</span>
|
||||
<em>${visibleCount}/${rows.length}</em>
|
||||
<i></i>
|
||||
</button>
|
||||
<div class="stl-family-list">
|
||||
${rows.map(({ file, index }) => {
|
||||
const id = normalizedStlId(file);
|
||||
const mode = stlMode(file);
|
||||
const color = stlColor(file, index);
|
||||
return `
|
||||
<button class="stl-row stl-mode-${mode} ${mode !== "hidden" ? "active" : ""}" type="button" data-stl-mode="${id}" style="--stl-color:${escapeHtml(color)}" title="${escapeHtml(file.file_name)}">
|
||||
<span class="stl-row-main">
|
||||
<strong>${escapeHtml(file.segment_name || file.file_name)}</strong>
|
||||
<small>${escapeHtml(file.family || "未分类")} · ${escapeHtml(file.category || "")} · ${formatBytes(file.size_bytes)}</small>
|
||||
<small class="stl-file-name">${escapeHtml(file.file_name)}</small>
|
||||
</span>
|
||||
<span class="stl-visibility-swatch ${mode}" aria-label="${escapeHtml(STL_DISPLAY_LABELS[mode])}"></span>
|
||||
</button>
|
||||
`;
|
||||
}).join("")}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}).join("");
|
||||
$("stlList").innerHTML = [...grouped.values()]
|
||||
.sort((a, b) => a.order - b.order || a.label.localeCompare(b.label))
|
||||
.map((group) => {
|
||||
const rows = group.rows.sort((a, b) => (
|
||||
a.presentation.itemOrder - b.presentation.itemOrder
|
||||
|| String(a.presentation.itemLabel).localeCompare(String(b.presentation.itemLabel))
|
||||
|| a.index - b.index
|
||||
));
|
||||
const visibleCount = rows.filter(({ file }) => stlMode(file) !== "hidden").length;
|
||||
const collapsed = state.collapsedStlFamilies.has(group.key);
|
||||
return `
|
||||
<section class="stl-family ${group.hierarchy ? "stl-hierarchy-group" : ""} ${collapsed ? "collapsed" : ""}">
|
||||
<button class="stl-family-head" type="button" data-family-toggle="${escapeHtml(group.key)}" title="收起/展开 ${escapeHtml(group.label)}">
|
||||
<span>${escapeHtml(group.label)}</span>
|
||||
<em>${visibleCount}/${rows.length}</em>
|
||||
<i></i>
|
||||
</button>
|
||||
<div class="stl-family-list">
|
||||
${rows.map(({ file, index, presentation }) => {
|
||||
const id = normalizedStlId(file);
|
||||
const mode = stlMode(file);
|
||||
const color = stlColor(file, index);
|
||||
return `
|
||||
<button class="stl-row stl-mode-${mode} ${mode !== "hidden" ? "active" : ""}" type="button" data-stl-mode="${id}" style="--stl-color:${escapeHtml(color)}" title="${escapeHtml(file.file_name)}">
|
||||
<span class="stl-row-main">
|
||||
<strong>${escapeHtml(presentation.itemLabel || file.segment_name || file.file_name)}</strong>
|
||||
<small>${escapeHtml(file.family || "未分类")} · ${escapeHtml(file.category || "")} · ${formatBytes(file.size_bytes)}</small>
|
||||
<small class="stl-file-name">${escapeHtml(file.file_name)}</small>
|
||||
</span>
|
||||
<span class="stl-visibility-swatch ${mode}" aria-label="${escapeHtml(STL_DISPLAY_LABELS[mode])}"></span>
|
||||
</button>
|
||||
`;
|
||||
}).join("")}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}).join("");
|
||||
$("stlList").querySelectorAll("[data-family-toggle]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const family = button.dataset.familyToggle;
|
||||
if (state.collapsedStlFamilies.has(family)) state.collapsedStlFamilies.delete(family);
|
||||
else state.collapsedStlFamilies.add(family);
|
||||
const groupKey = button.dataset.familyToggle;
|
||||
if (state.collapsedStlFamilies.has(groupKey)) state.collapsedStlFamilies.delete(groupKey);
|
||||
else state.collapsedStlFamilies.add(groupKey);
|
||||
renderStl();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user