Refine DICOM UPP registration workstation layout
This commit is contained in:
@@ -44,6 +44,8 @@ const state = {
|
||||
statusFilter: "",
|
||||
partFilter: "",
|
||||
search: "",
|
||||
caseCollapsed: false,
|
||||
activeToolTab: "series",
|
||||
cases: [],
|
||||
activeCase: null,
|
||||
series: [],
|
||||
@@ -54,7 +56,10 @@ const state = {
|
||||
registrationStatus: "unregistered",
|
||||
pose: { ...DEFAULT_POSE },
|
||||
windowMode: "default",
|
||||
fusionMode: "fusion",
|
||||
sliceIndex: 0,
|
||||
dicomPreviewTimer: 0,
|
||||
autoResult: null,
|
||||
dirty: false,
|
||||
saving: false,
|
||||
fusionVersion: 0,
|
||||
@@ -68,7 +73,9 @@ const state = {
|
||||
modelGroup: null,
|
||||
rawModelGroup: null,
|
||||
baseModelScale: 1,
|
||||
dicomSceneBox: null,
|
||||
resizeObserver: null,
|
||||
resizeScene: null,
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
@@ -98,6 +105,13 @@ function setSaveState(text, tone = "") {
|
||||
el.className = tone;
|
||||
}
|
||||
|
||||
function setAutoState(text, tone = "") {
|
||||
const el = $("autoState");
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.className = tone;
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
state.dirty = true;
|
||||
setSaveState("未保存", "warn");
|
||||
@@ -298,9 +312,16 @@ function clearDetail() {
|
||||
$("activeTitle").textContent = "未选择 CT";
|
||||
$("activeMeta").textContent = "从左侧选择一个完整关联检查";
|
||||
$("activeTags").innerHTML = "";
|
||||
$("patientSummary").textContent = "未选择 CT";
|
||||
$("summaryCt").textContent = "-";
|
||||
$("summaryPatientId").textContent = "-";
|
||||
$("summaryStudyTime").textContent = "-";
|
||||
$("summaryBodyPart").textContent = "-";
|
||||
$("matchedSeries").innerHTML = "";
|
||||
$("seriesList").innerHTML = `<div class="empty-state">未选择 CT</div>`;
|
||||
$("stlList").innerHTML = `<div class="empty-state">未选择 STL</div>`;
|
||||
$("modelRail").innerHTML = "";
|
||||
renderDicomAnnotation();
|
||||
clearFusion();
|
||||
}
|
||||
|
||||
@@ -327,6 +348,7 @@ async function selectCase(ctNumber, algorithmModel = "未指定模型") {
|
||||
state.stlFiles = stlPayload.files || [];
|
||||
state.registrationStatus = registration.registration_status || "unregistered";
|
||||
state.pose = { ...DEFAULT_POSE, ...(registration.transform || {}) };
|
||||
state.autoResult = null;
|
||||
state.sliceIndex = 0;
|
||||
$("notes").value = registration.notes || "";
|
||||
|
||||
@@ -389,34 +411,43 @@ function renderActiveHeader() {
|
||||
$("activeTags").innerHTML = tags.join("");
|
||||
$("statusBtn").textContent = state.registrationStatus === "registered" ? "标为未配准" : "标为已配准";
|
||||
$("statusBtn").classList.toggle("unregistered", state.registrationStatus !== "registered");
|
||||
$("patientSummary").textContent = `${row.patient_name || row.upp_patient_name || "-"} ${row.patient_sex || ""}`.trim();
|
||||
$("summaryCt").textContent = row.ct_number || "-";
|
||||
$("summaryPatientId").textContent = row.patient_id || "-";
|
||||
$("summaryStudyTime").textContent = fmtDateTime(row.study_date, row.study_time) || "-";
|
||||
$("summaryBodyPart").textContent = bodyPartTags(row).join("、") || row.study_description || "-";
|
||||
}
|
||||
|
||||
function renderSeries() {
|
||||
$("seriesCount").textContent = state.series.length;
|
||||
const activeSeries = currentSeries();
|
||||
$("matchedSeries").innerHTML = activeSeries ? seriesCardHtml(activeSeries, true) : `<div class="empty-state">尚未选择配准序列</div>`;
|
||||
$("seriesList").innerHTML = state.series.length
|
||||
? state.series
|
||||
.map((item) => {
|
||||
const active = item.series_uid === state.selectedSeriesUid;
|
||||
const labels = (item.annotation_labels || []).map((label) => `<i>${escapeHtml(label)}</i>`).join("");
|
||||
const time = [fmtTime(item.first_time || item.series_time), fmtTime(item.last_time)].filter(Boolean);
|
||||
return `
|
||||
<button class="series-card ${active ? "active" : ""}" data-series="${escapeHtml(item.series_uid)}" title="${escapeHtml(item.description)}">
|
||||
<strong>${escapeHtml(item.description || "未命名序列")}</strong>
|
||||
<span>拍摄 ${escapeHtml(time.length > 1 ? `${time[0]}-${time[1]}` : time[0] || "-")}</span>
|
||||
<small>序列 ${escapeHtml(item.series_number || "-")} · ${escapeHtml(item.rows || "-")}×${escapeHtml(item.columns || "-")} · ${escapeHtml(item.modality || "CT")} · ${Number(item.count || 0)} 张</small>
|
||||
<div class="mini-tags">
|
||||
${item.body_part_dicom ? `<i>${escapeHtml(item.body_part_dicom)}</i>` : ""}
|
||||
${labels || `<i>未标注</i>`}
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.map((item) => seriesCardHtml(item, item.series_uid === state.selectedSeriesUid))
|
||||
.join("")
|
||||
: `<div class="empty-state">未读取到 DICOM 序列</div>`;
|
||||
$("seriesList").querySelectorAll(".series-card").forEach((button) => {
|
||||
document.querySelectorAll(".series-card[data-series]").forEach((button) => {
|
||||
button.addEventListener("click", () => selectSeries(button.dataset.series));
|
||||
});
|
||||
updateSliceControl();
|
||||
renderDicomAnnotation();
|
||||
}
|
||||
|
||||
function seriesCardHtml(item, active = false) {
|
||||
const labels = (item.annotation_labels || []).map((label) => `<i>${escapeHtml(label)}</i>`).join("");
|
||||
const time = [fmtTime(item.first_time || item.series_time), fmtTime(item.last_time)].filter(Boolean);
|
||||
return `
|
||||
<button class="series-card ${active ? "active" : ""}" data-series="${escapeHtml(item.series_uid)}" title="${escapeHtml(item.description)}">
|
||||
<strong>${escapeHtml(item.description || "未命名序列")}</strong>
|
||||
<span>拍摄 ${escapeHtml(time.length > 1 ? `${time[0]}-${time[1]}` : time[0] || "-")}</span>
|
||||
<small>${escapeHtml(item.slice_thickness || "厚度未知")} · 序列 ${escapeHtml(item.series_number || "-")} · ${escapeHtml(item.rows || "-")}×${escapeHtml(item.columns || "-")} · ${escapeHtml(item.modality || "CT")} · ${Number(item.count || 0)} 张</small>
|
||||
<div class="mini-tags">
|
||||
${item.body_part_dicom ? `<i>${escapeHtml(item.body_part_dicom)}</i>` : ""}
|
||||
${labels || `<i>未标注</i>`}
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
async function selectSeries(uid) {
|
||||
@@ -427,6 +458,7 @@ async function selectSeries(uid) {
|
||||
state.sliceIndex = selected ? Math.max(0, Math.floor((Number(selected.count) || 1) / 2)) : 0;
|
||||
markDirty();
|
||||
renderSeries();
|
||||
renderDicomAnnotation();
|
||||
await loadFusion();
|
||||
}
|
||||
|
||||
@@ -479,8 +511,11 @@ function buildPoseControls() {
|
||||
(item) => `
|
||||
<div class="pose-control" data-pose="${item.key}">
|
||||
<label>${item.label}</label>
|
||||
<button type="button" data-nudge="${item.key}" data-delta="${-item.step}">-</button>
|
||||
<input type="range" min="${item.min}" max="${item.max}" step="${item.step}" />
|
||||
<button type="button" data-nudge="${item.key}" data-delta="${item.step}">+</button>
|
||||
<input type="number" min="${item.min}" max="${item.max}" step="${item.step}" />
|
||||
${item.key.startsWith("rotate") ? `<div class="quick-row"><button type="button" data-nudge="${item.key}" data-delta="-90">-90°</button><button type="button" data-nudge="${item.key}" data-delta="90">+90°</button></div>` : ""}
|
||||
</div>
|
||||
`,
|
||||
).join("");
|
||||
@@ -499,6 +534,9 @@ function buildPoseControls() {
|
||||
range.addEventListener("input", () => sync(range.value, range));
|
||||
number.addEventListener("input", () => sync(number.value, number));
|
||||
});
|
||||
document.querySelectorAll("[data-nudge]").forEach((button) => {
|
||||
button.addEventListener("click", () => nudgePose(button.dataset.nudge, Number(button.dataset.delta || 0)));
|
||||
});
|
||||
document.querySelectorAll("[data-flip]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const key = button.dataset.flip;
|
||||
@@ -510,6 +548,17 @@ function buildPoseControls() {
|
||||
});
|
||||
}
|
||||
|
||||
function nudgePose(key, delta) {
|
||||
if (!key || !Number.isFinite(delta)) return;
|
||||
const option = POSE_CONTROLS.find((item) => item.key === key);
|
||||
const current = Number(state.pose[key] ?? DEFAULT_POSE[key]);
|
||||
const next = Math.max(option?.min ?? -Infinity, Math.min(option?.max ?? Infinity, current + delta));
|
||||
state.pose[key] = Number(next.toFixed(3));
|
||||
renderPoseControls();
|
||||
applyPose();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function renderPoseControls() {
|
||||
POSE_CONTROLS.forEach((item) => {
|
||||
const row = $(`poseGrid`).querySelector(`[data-pose="${item.key}"]`);
|
||||
@@ -532,6 +581,46 @@ function updateSliceControl() {
|
||||
$("sliceLabel").textContent = count ? `切片 ${Number($("sliceSlider").value) + 1} / ${count}` : "切片 0 / 0";
|
||||
}
|
||||
|
||||
function renderDicomAnnotation() {
|
||||
const row = state.activeCase;
|
||||
const selected = currentSeries();
|
||||
const count = Number(selected?.count || 0);
|
||||
$("dicomPanelMeta").textContent = selected ? `${selected.description || "未命名序列"} · ${count} 张` : "当前序列切片";
|
||||
$("dicomInfoLeft").textContent = row && selected
|
||||
? `${row.patient_name || row.upp_patient_name || "-"}\n${row.patient_id || "-"}\n${fmtDateTime(row.study_date, row.study_time) || "-"}\n${selected.description || "-"}`
|
||||
: "未选择序列";
|
||||
$("dicomInfoRight").textContent = selected
|
||||
? `${selected.modality || "CT"}\n${selected.rows || "-"}×${selected.columns || "-"}\n${selected.slice_thickness || "厚度 -"}`
|
||||
: "";
|
||||
$("dicomInfoBottom").textContent = count ? `Img:${state.sliceIndex + 1}/${count}` : "Img:-";
|
||||
const tags = selected?.annotation_labels?.length ? selected.annotation_labels : bodyPartTags(row || {});
|
||||
$("annotationTags").innerHTML = tags.length
|
||||
? tags.map((tag) => `<span>${escapeHtml(tag)}</span>`).join("")
|
||||
: `<span>未标注</span>`;
|
||||
}
|
||||
|
||||
function updateDicomPreview() {
|
||||
const selected = currentSeries();
|
||||
if (!state.activeCase || !selected) {
|
||||
$("dicomPreview").removeAttribute("src");
|
||||
renderDicomAnnotation();
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(state.dicomPreviewTimer);
|
||||
state.dicomPreviewTimer = window.setTimeout(() => {
|
||||
const params = new URLSearchParams({
|
||||
ct_number: state.activeCase.ct_number,
|
||||
series_uid: selected.series_uid,
|
||||
index: String(state.sliceIndex),
|
||||
window: state.windowMode,
|
||||
access_token: state.token,
|
||||
});
|
||||
$("dicomLoading").classList.remove("hidden");
|
||||
$("dicomPreview").src = `/api/dicom/image?${params.toString()}`;
|
||||
renderDicomAnnotation();
|
||||
}, 80);
|
||||
}
|
||||
|
||||
function initScene() {
|
||||
if (state.sceneReady) return;
|
||||
const viewport = $("fusionViewport");
|
||||
@@ -570,6 +659,7 @@ function initScene() {
|
||||
camera.aspect = rect.width / rect.height;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
state.resizeScene = resize;
|
||||
state.resizeObserver = new ResizeObserver(resize);
|
||||
state.resizeObserver.observe(viewport);
|
||||
resize();
|
||||
@@ -606,6 +696,14 @@ function clearFusion() {
|
||||
updateSliceControl();
|
||||
}
|
||||
|
||||
function applyFusionMode() {
|
||||
if (state.dicomGroup) state.dicomGroup.visible = state.fusionMode !== "model";
|
||||
if (state.modelGroup) state.modelGroup.visible = true;
|
||||
$("fusionMeta").textContent = state.fusionMode === "model"
|
||||
? `单独模型 · STL ${state.selectedStlIds.size} 个`
|
||||
: $("fusionMeta").textContent;
|
||||
}
|
||||
|
||||
async function loadFusion() {
|
||||
const version = ++state.fusionVersion;
|
||||
const selected = currentSeries();
|
||||
@@ -634,9 +732,12 @@ async function loadFusion() {
|
||||
if (version !== state.fusionVersion) return;
|
||||
await buildStlModels(volume);
|
||||
if (version !== state.fusionVersion) return;
|
||||
applyFusionMode();
|
||||
fitCamera();
|
||||
$("fusionStatus").textContent = `${selected.description || "DICOM 序列"} · ${Number(selected.count || 0)} 张`;
|
||||
$("fusionMeta").textContent = `DICOM ${volume.indices?.length || 0} 张局部体 · STL ${state.selectedStlIds.size} 个`;
|
||||
renderDicomAnnotation();
|
||||
updateDicomPreview();
|
||||
} catch (error) {
|
||||
clearFusion();
|
||||
$("fusionStatus").textContent = error.message;
|
||||
@@ -682,6 +783,7 @@ async function buildDicomVolume(volume) {
|
||||
const lines = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x1cd9c7, transparent: true, opacity: 0.42 }));
|
||||
state.dicomGroup.add(lines);
|
||||
state.baseModelScale = sceneScale;
|
||||
state.dicomSceneBox = new THREE.Box3().setFromObject(state.dicomGroup);
|
||||
}
|
||||
|
||||
async function buildStlModels(volume) {
|
||||
@@ -723,9 +825,9 @@ async function buildStlModels(volume) {
|
||||
applyPose();
|
||||
}
|
||||
|
||||
function applyPose() {
|
||||
function applyPose(poseInput = state.pose) {
|
||||
if (!state.modelGroup) return;
|
||||
const pose = { ...DEFAULT_POSE, ...state.pose };
|
||||
const pose = { ...DEFAULT_POSE, ...poseInput };
|
||||
state.modelGroup.rotation.set(
|
||||
THREE.MathUtils.degToRad(Number(pose.rotateX) || 0),
|
||||
THREE.MathUtils.degToRad(Number(pose.rotateY) || 0),
|
||||
@@ -751,6 +853,120 @@ function fitCamera() {
|
||||
state.controls.update();
|
||||
}
|
||||
|
||||
function resetPose(kind) {
|
||||
if (kind === "rotation") {
|
||||
state.pose = { ...state.pose, rotateX: 0, rotateY: 0, rotateZ: 0 };
|
||||
} else if (kind === "transform") {
|
||||
state.pose = { ...state.pose, translateX: 0, translateY: 0, translateZ: 0, scale: 1 };
|
||||
} else if (kind === "flip") {
|
||||
state.pose = { ...state.pose, flipX: false, flipY: false, flipZ: false };
|
||||
} else {
|
||||
state.pose = { ...DEFAULT_POSE };
|
||||
}
|
||||
renderPoseControls();
|
||||
applyPose();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function suggestBoneSelection() {
|
||||
const boneKeys = ["rib", "vertebra", "sternum", "bone", "spine", "肋", "椎", "骨", "胸骨"];
|
||||
const next = new Set();
|
||||
state.stlFiles.forEach((file) => {
|
||||
const haystack = `${file.family || ""} ${file.segment_name || ""} ${file.file_name || ""} ${file.category || ""}`.toLowerCase();
|
||||
if (boneKeys.some((key) => haystack.includes(key))) next.add(Number(file.id));
|
||||
});
|
||||
if (!next.size) {
|
||||
state.stlFiles.slice(0, Math.min(6, state.stlFiles.length)).forEach((file) => next.add(Number(file.id)));
|
||||
}
|
||||
state.selectedStlIds = next;
|
||||
renderStl();
|
||||
markDirty();
|
||||
loadFusion();
|
||||
}
|
||||
|
||||
function candidateScore(pose) {
|
||||
if (!state.dicomSceneBox || !state.modelGroup || !state.rawModelGroup?.children.length) return -Infinity;
|
||||
const current = { ...state.pose };
|
||||
applyPose(pose);
|
||||
const modelBox = new THREE.Box3().setFromObject(state.modelGroup);
|
||||
applyPose(current);
|
||||
const overlap = modelBox.clone().intersect(state.dicomSceneBox);
|
||||
const overlapSize = new THREE.Vector3();
|
||||
overlap.getSize(overlapSize);
|
||||
const modelSize = new THREE.Vector3();
|
||||
const dicomSize = new THREE.Vector3();
|
||||
modelBox.getSize(modelSize);
|
||||
state.dicomSceneBox.getSize(dicomSize);
|
||||
const overlapVolume = Math.max(0, overlapSize.x) * Math.max(0, overlapSize.y) * Math.max(0, overlapSize.z);
|
||||
const modelVolume = Math.max(modelSize.x * modelSize.y * modelSize.z, 0.001);
|
||||
const modelCenter = new THREE.Vector3();
|
||||
const dicomCenter = new THREE.Vector3();
|
||||
modelBox.getCenter(modelCenter);
|
||||
state.dicomSceneBox.getCenter(dicomCenter);
|
||||
const centerPenalty = modelCenter.distanceTo(dicomCenter) / Math.max(dicomSize.length(), 0.001);
|
||||
const scalePenalty = Math.abs((pose.scale || 1) - 1) * 0.03;
|
||||
return overlapVolume / modelVolume - centerPenalty - scalePenalty;
|
||||
}
|
||||
|
||||
function runAutoCoarse() {
|
||||
if (!state.activeCase || !currentSeries() || !state.rawModelGroup?.children.length) {
|
||||
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
|
||||
return;
|
||||
}
|
||||
state.pose = { ...state.pose, translateX: 0, translateY: 0, translateZ: 0, scale: 1 };
|
||||
renderPoseControls();
|
||||
applyPose();
|
||||
fitCamera();
|
||||
state.autoResult = { score: candidateScore(state.pose), pose: { ...state.pose }, evaluated: 1 };
|
||||
setAutoState("粗配准完成", "ok");
|
||||
$("autoResult").textContent = `已按 DICOM 物理尺寸复位平移/缩放,score ${state.autoResult.score.toFixed(4)}`;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function runAutoFine() {
|
||||
if (!state.activeCase || !currentSeries() || !state.rawModelGroup?.children.length) {
|
||||
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
|
||||
return;
|
||||
}
|
||||
setAutoState("运行中", "warn");
|
||||
$("autoResult").textContent = "正在进行几何重叠微调...";
|
||||
const adjustable = {
|
||||
translateX: $("autoX").checked,
|
||||
translateY: $("autoY").checked,
|
||||
translateZ: $("autoZ").checked,
|
||||
scale: $("autoScale").checked,
|
||||
};
|
||||
let best = { pose: { ...state.pose }, score: candidateScore(state.pose), mode: "初始位姿" };
|
||||
let evaluated = 1;
|
||||
const rounds = [
|
||||
{ move: 0.34, scale: 0.12 },
|
||||
{ move: 0.18, scale: 0.06 },
|
||||
{ move: 0.08, scale: 0.025 },
|
||||
{ move: 0.035, scale: 0.01 },
|
||||
];
|
||||
for (const round of rounds) {
|
||||
const candidates = [];
|
||||
if (adjustable.translateX) candidates.push(["translateX", round.move], ["translateX", -round.move]);
|
||||
if (adjustable.translateY) candidates.push(["translateY", round.move], ["translateY", -round.move]);
|
||||
if (adjustable.translateZ) candidates.push(["translateZ", round.move], ["translateZ", -round.move]);
|
||||
if (adjustable.scale) candidates.push(["scale", round.scale], ["scale", -round.scale]);
|
||||
for (const [key, delta] of candidates) {
|
||||
const pose = { ...best.pose, [key]: key === "scale" ? Math.max(0.2, Math.min(3, best.pose.scale + delta)) : best.pose[key] + delta };
|
||||
const score = candidateScore(pose);
|
||||
evaluated += 1;
|
||||
if (score > best.score) best = { pose, score, mode: `${key} ${delta > 0 ? "+" : "-"}` };
|
||||
}
|
||||
}
|
||||
state.pose = { ...best.pose };
|
||||
state.autoResult = { ...best, evaluated };
|
||||
renderPoseControls();
|
||||
applyPose();
|
||||
fitCamera();
|
||||
setAutoState("微调完成", "ok");
|
||||
$("autoResult").textContent = `最佳候选:${best.mode};score ${best.score.toFixed(4)};评估 ${evaluated} 个候选。`;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
async function saveRegistration(nextStatus = null) {
|
||||
if (!state.activeCase || !currentSeries() || state.saving) return;
|
||||
state.saving = true;
|
||||
@@ -792,6 +1008,8 @@ async function saveRegistration(nextStatus = null) {
|
||||
algorithm_model: state.algorithmModel,
|
||||
selected_count: selectedFiles.length,
|
||||
families: [...new Set(selectedFiles.map((file) => file.family).filter(Boolean))],
|
||||
fusion_mode: state.fusionMode,
|
||||
auto_result: state.autoResult,
|
||||
},
|
||||
notes: $("notes").value,
|
||||
}),
|
||||
@@ -846,16 +1064,42 @@ function openLinkedApp(kind) {
|
||||
function wireEvents() {
|
||||
$("loginForm").addEventListener("submit", login);
|
||||
$("logoutBtn").addEventListener("click", () => logout());
|
||||
$("caseToggleBtn").addEventListener("click", () => {
|
||||
state.caseCollapsed = !state.caseCollapsed;
|
||||
document.querySelector(".workspace").classList.toggle("case-collapsed", state.caseCollapsed);
|
||||
$("caseToggleBtn").classList.toggle("active", state.caseCollapsed);
|
||||
window.setTimeout(() => {
|
||||
state.resizeScene?.();
|
||||
fitCamera();
|
||||
}, 230);
|
||||
});
|
||||
$("refreshBtn").addEventListener("click", async () => {
|
||||
await loadStatus();
|
||||
await loadCases(false);
|
||||
});
|
||||
$("relationBtn").addEventListener("click", () => openLinkedApp("relation"));
|
||||
$("viewerBtn").addEventListener("click", () => openLinkedApp("viewer"));
|
||||
$("openViewerBtn").addEventListener("click", () => openLinkedApp("viewer"));
|
||||
$("reloadFusionBtn").addEventListener("click", () => loadFusion());
|
||||
$("fitBtn").addEventListener("click", fitCamera);
|
||||
$("saveBtn").addEventListener("click", () => saveRegistration());
|
||||
$("statusBtn").addEventListener("click", () => saveRegistration(state.registrationStatus === "registered" ? "unregistered" : "registered"));
|
||||
$("notes").addEventListener("input", markDirty);
|
||||
$("resetRotationBtn").addEventListener("click", () => resetPose("rotation"));
|
||||
$("resetTransformBtn").addEventListener("click", () => resetPose("transform"));
|
||||
$("resetFlipBtn").addEventListener("click", () => resetPose("flip"));
|
||||
$("suggestBoneBtn").addEventListener("click", suggestBoneSelection);
|
||||
$("autoCoarseBtn").addEventListener("click", runAutoCoarse);
|
||||
$("autoFineBtn").addEventListener("click", runAutoFine);
|
||||
$("dicomPreview").addEventListener("load", () => $("dicomLoading").classList.add("hidden"));
|
||||
$("dicomPreview").addEventListener("error", () => $("dicomLoading").classList.add("hidden"));
|
||||
document.querySelectorAll("[data-tool-tab]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
state.activeToolTab = button.dataset.toolTab || "series";
|
||||
document.querySelectorAll("[data-tool-tab]").forEach((item) => item.classList.toggle("active", item === button));
|
||||
document.querySelectorAll("[data-tool-pane]").forEach((pane) => pane.classList.toggle("active", pane.dataset.toolPane === state.activeToolTab));
|
||||
});
|
||||
});
|
||||
$("caseSearch").addEventListener("input", () => {
|
||||
state.search = $("caseSearch").value.trim();
|
||||
window.clearTimeout(state.searchTimer);
|
||||
@@ -875,16 +1119,26 @@ function wireEvents() {
|
||||
await loadCases(false);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".segmented button").forEach((button) => {
|
||||
document.querySelectorAll("[data-window]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
state.windowMode = button.dataset.window || "default";
|
||||
document.querySelectorAll(".segmented button").forEach((item) => item.classList.toggle("active", item === button));
|
||||
document.querySelectorAll("[data-window]").forEach((item) => item.classList.toggle("active", item === button));
|
||||
await loadFusion();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-fusion-mode]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
state.fusionMode = button.dataset.fusionMode || "fusion";
|
||||
document.querySelectorAll("[data-fusion-mode]").forEach((item) => item.classList.toggle("active", item === button));
|
||||
applyFusionMode();
|
||||
fitCamera();
|
||||
});
|
||||
});
|
||||
$("sliceSlider").addEventListener("input", () => {
|
||||
state.sliceIndex = Number($("sliceSlider").value || 0);
|
||||
updateSliceControl();
|
||||
renderDicomAnnotation();
|
||||
updateDicomPreview();
|
||||
});
|
||||
$("sliceSlider").addEventListener("change", () => loadFusion());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user