diff --git a/DICOM_and_UPP配准/static/app.js b/DICOM_and_UPP配准/static/app.js index 37a41fa..b0f2edf 100644 --- a/DICOM_and_UPP配准/static/app.js +++ b/DICOM_and_UPP配准/static/app.js @@ -411,10 +411,22 @@ function stlSelectionSignature() { return [ normalize(state.activeCase?.ct_number), state.algorithmModel || "", - [...state.selectedStlIds].map(Number).sort((a, b) => a - b).join(","), + state.stlFiles.map((file) => `${Number(file.id)}:${file.file_name || ""}:${file.size_bytes || 0}`).join(","), ].join("|"); } +function visibleModelRecords() { + return state.modelMeshes.filter((record) => state.selectedStlIds.has(Number(record.file?.id)) && record.mesh?.visible !== false); +} + +function visibleModelBox() { + const records = visibleModelRecords(); + if (!records.length) return null; + const box = new THREE.Box3(); + records.forEach((record) => box.expandByObject(record.mesh)); + return box; +} + function bodyPartTags(row) { const labels = Array.isArray(row?.body_part_labels) ? row.body_part_labels : []; const keys = Array.isArray(row?.body_part_keys) ? row.body_part_keys : []; @@ -708,9 +720,7 @@ function renderStl() { const id = Number(input.dataset.stl); if (input.checked) state.selectedStlIds.add(id); else state.selectedStlIds.delete(id); - markDirty("stl"); - renderStl(); - await loadFusion(); + await applyStlSelectionChange(); }); }); renderAutoBoneOptions(); @@ -719,9 +729,7 @@ function renderStl() { async function selectAllStl() { if (!state.stlFiles.length) return; state.selectedStlIds = new Set(state.stlFiles.map((file) => Number(file.id)).filter(Number.isFinite)); - markDirty("stl"); - renderStl(); - await loadFusion(); + await applyStlSelectionChange(); } async function invertStlSelection() { @@ -732,9 +740,19 @@ async function invertStlSelection() { if (Number.isFinite(id) && !state.selectedStlIds.has(id)) next.add(id); }); state.selectedStlIds = next; + await applyStlSelectionChange(); +} + +async function applyStlSelectionChange() { markDirty("stl"); renderStl(); - await loadFusion(); + if (!state.modelMeshes.length) { + await loadFusion(); + return; + } + applyStlVisibility(); + applyModelDetail(); + scheduleMappingDraw(); } function stlLikelyScore(file) { @@ -1094,9 +1112,19 @@ function clearFusion() { 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; + updateFusionMeta(); +} + +function updateFusionMeta() { + const selectedCount = visibleModelRecords().length || state.selectedStlIds.size; + if (state.fusionMode === "model") { + $("fusionMeta").textContent = `单独模型 · STL ${selectedCount} 个`; + return; + } + const volume = state.volumeMeta; + $("fusionMeta").textContent = volume + ? `DICOM ${volume.start + 1}-${volume.end + 1}/${volume.total} · STL ${selectedCount} 个` + : `DICOM - · STL ${selectedCount} 个`; } function fusionDetailSettings() { @@ -1150,6 +1178,18 @@ function applyModelDetail() { scheduleMappingDraw(); } +function applyStlVisibility() { + const selected = state.selectedStlIds; + state.modelMeshes.forEach((record) => { + record.mesh.visible = selected.has(Number(record.file?.id)); + }); + if (state.modelBoundsFrame) state.modelBoundsFrame.visible = state.modelMeshes.some((record) => record.mesh.visible); + if (state.dicomGroup) state.dicomGroup.visible = state.fusionMode !== "model"; + if (state.modelGroup) state.modelGroup.visible = true; + updateFusionMeta(); + scheduleMappingDraw(); +} + function syncDisplayControls() { document.querySelectorAll("[data-fusion-detail]").forEach((item) => item.classList.toggle("active", item.dataset.fusionDetail === state.fusionDetail)); document.querySelectorAll("[data-model-detail]").forEach((item) => item.classList.toggle("active", item.dataset.modelDetail === state.modelDetail)); @@ -1193,7 +1233,7 @@ async function loadFusion() { $("fusionStatus").textContent = state.sceneReady ? "三维融合场景已就绪" : "2D 映射已就绪,当前浏览器未启用 WebGL"; - $("fusionMeta").textContent = `DICOM ${volume.start + 1}-${volume.end + 1}/${volume.total} · STL ${state.selectedStlIds.size} 个`; + updateFusionMeta(); renderDicomAnnotation(); setFusionLoading(true, "正在渲染右侧分割映射", 96); updateDicomPreview(); @@ -1420,8 +1460,9 @@ function drawMappingView() { resetMappingCanvas("单独影像模式未叠加 STL 分割"); return; } - if (!image?.complete || !image.naturalWidth || !volumeScene || !state.modelMeshes.length) { - resetMappingCanvas(state.modelMeshes.length ? "等待 DICOM Base Layer" : "当前没有可见 STL 构件"); + const visibleRecords = visibleModelRecords(); + if (!image?.complete || !image.naturalWidth || !volumeScene || !visibleRecords.length) { + resetMappingCanvas(visibleRecords.length ? "等待 DICOM Base Layer" : "当前没有可见 STL 构件"); return; } @@ -1447,7 +1488,7 @@ function drawMappingView() { const tempB = new THREE.Vector3(); const tempC = new THREE.Vector3(); - state.modelMeshes.forEach((record, index) => { + visibleRecords.forEach((record, index) => { const position = record.mesh.geometry.attributes.position; if (!position) return; const triangleCount = Math.floor(position.count / 3); @@ -1487,7 +1528,7 @@ function drawMappingView() { } }); - $("mappingStats").textContent = `${activeModules}/${state.modelMeshes.length} 构件 · ${segmentCount} 边 · ${filledPixels} px`; + $("mappingStats").textContent = `${activeModules}/${visibleRecords.length} 构件 · ${segmentCount} 边 · ${filledPixels} px`; $("mappingLegend").innerHTML = modules.length ? modules.map((item) => `