Cache STL workspace visibility toggles

This commit is contained in:
Codex
2026-05-29 09:55:13 +08:00
parent 45e719aab0
commit 989303ec35

View File

@@ -411,10 +411,22 @@ function stlSelectionSignature() {
return [ return [
normalize(state.activeCase?.ct_number), normalize(state.activeCase?.ct_number),
state.algorithmModel || "", 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("|"); ].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) { function bodyPartTags(row) {
const labels = Array.isArray(row?.body_part_labels) ? row.body_part_labels : []; const labels = Array.isArray(row?.body_part_labels) ? row.body_part_labels : [];
const keys = Array.isArray(row?.body_part_keys) ? row.body_part_keys : []; const keys = Array.isArray(row?.body_part_keys) ? row.body_part_keys : [];
@@ -708,9 +720,7 @@ function renderStl() {
const id = Number(input.dataset.stl); const id = Number(input.dataset.stl);
if (input.checked) state.selectedStlIds.add(id); if (input.checked) state.selectedStlIds.add(id);
else state.selectedStlIds.delete(id); else state.selectedStlIds.delete(id);
markDirty("stl"); await applyStlSelectionChange();
renderStl();
await loadFusion();
}); });
}); });
renderAutoBoneOptions(); renderAutoBoneOptions();
@@ -719,9 +729,7 @@ function renderStl() {
async function selectAllStl() { async function selectAllStl() {
if (!state.stlFiles.length) return; if (!state.stlFiles.length) return;
state.selectedStlIds = new Set(state.stlFiles.map((file) => Number(file.id)).filter(Number.isFinite)); state.selectedStlIds = new Set(state.stlFiles.map((file) => Number(file.id)).filter(Number.isFinite));
markDirty("stl"); await applyStlSelectionChange();
renderStl();
await loadFusion();
} }
async function invertStlSelection() { async function invertStlSelection() {
@@ -732,9 +740,19 @@ async function invertStlSelection() {
if (Number.isFinite(id) && !state.selectedStlIds.has(id)) next.add(id); if (Number.isFinite(id) && !state.selectedStlIds.has(id)) next.add(id);
}); });
state.selectedStlIds = next; state.selectedStlIds = next;
await applyStlSelectionChange();
}
async function applyStlSelectionChange() {
markDirty("stl"); markDirty("stl");
renderStl(); renderStl();
await loadFusion(); if (!state.modelMeshes.length) {
await loadFusion();
return;
}
applyStlVisibility();
applyModelDetail();
scheduleMappingDraw();
} }
function stlLikelyScore(file) { function stlLikelyScore(file) {
@@ -1094,9 +1112,19 @@ function clearFusion() {
function applyFusionMode() { function applyFusionMode() {
if (state.dicomGroup) state.dicomGroup.visible = state.fusionMode !== "model"; if (state.dicomGroup) state.dicomGroup.visible = state.fusionMode !== "model";
if (state.modelGroup) state.modelGroup.visible = true; if (state.modelGroup) state.modelGroup.visible = true;
$("fusionMeta").textContent = state.fusionMode === "model" updateFusionMeta();
? `单独模型 · STL ${state.selectedStlIds.size}` }
: $("fusionMeta").textContent;
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() { function fusionDetailSettings() {
@@ -1150,6 +1178,18 @@ function applyModelDetail() {
scheduleMappingDraw(); 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() { function syncDisplayControls() {
document.querySelectorAll("[data-fusion-detail]").forEach((item) => item.classList.toggle("active", item.dataset.fusionDetail === state.fusionDetail)); 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)); 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 $("fusionStatus").textContent = state.sceneReady
? "三维融合场景已就绪" ? "三维融合场景已就绪"
: "2D 映射已就绪,当前浏览器未启用 WebGL"; : "2D 映射已就绪,当前浏览器未启用 WebGL";
$("fusionMeta").textContent = `DICOM ${volume.start + 1}-${volume.end + 1}/${volume.total} · STL ${state.selectedStlIds.size}`; updateFusionMeta();
renderDicomAnnotation(); renderDicomAnnotation();
setFusionLoading(true, "正在渲染右侧分割映射", 96); setFusionLoading(true, "正在渲染右侧分割映射", 96);
updateDicomPreview(); updateDicomPreview();
@@ -1420,8 +1460,9 @@ function drawMappingView() {
resetMappingCanvas("单独影像模式未叠加 STL 分割"); resetMappingCanvas("单独影像模式未叠加 STL 分割");
return; return;
} }
if (!image?.complete || !image.naturalWidth || !volumeScene || !state.modelMeshes.length) { const visibleRecords = visibleModelRecords();
resetMappingCanvas(state.modelMeshes.length ? "等待 DICOM Base Layer" : "当前没有可见 STL 构件"); if (!image?.complete || !image.naturalWidth || !volumeScene || !visibleRecords.length) {
resetMappingCanvas(visibleRecords.length ? "等待 DICOM Base Layer" : "当前没有可见 STL 构件");
return; return;
} }
@@ -1447,7 +1488,7 @@ function drawMappingView() {
const tempB = new THREE.Vector3(); const tempB = new THREE.Vector3();
const tempC = new THREE.Vector3(); const tempC = new THREE.Vector3();
state.modelMeshes.forEach((record, index) => { visibleRecords.forEach((record, index) => {
const position = record.mesh.geometry.attributes.position; const position = record.mesh.geometry.attributes.position;
if (!position) return; if (!position) return;
const triangleCount = Math.floor(position.count / 3); 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 $("mappingLegend").innerHTML = modules.length
? modules.map((item) => ` ? modules.map((item) => `
<div class="legend-item" title="${escapeHtml(item.name)}"> <div class="legend-item" title="${escapeHtml(item.name)}">
@@ -1667,9 +1708,10 @@ async function buildStlModels(volume, onProgress = null) {
const signature = stlSelectionSignature(); const signature = stlSelectionSignature();
if (signature && signature === state.stlSignature && state.rawModelGroup?.children.length) { if (signature && signature === state.stlSignature && state.rawModelGroup?.children.length) {
updateBaseModelScaleFromBounds(); updateBaseModelScaleFromBounds();
if (!state.modelBoundsFrame && state.modelLocalBounds?.size) addModelBoundsFrame(state.modelLocalBounds.size);
applyStlVisibility();
applyPose(); applyPose();
applyModelDetail(); applyModelDetail();
if (!state.modelBoundsFrame && state.modelLocalBounds?.size) addModelBoundsFrame(state.modelLocalBounds.size);
scheduleMappingDraw(); scheduleMappingDraw();
onProgress?.(100); onProgress?.(100);
return; return;
@@ -1682,7 +1724,7 @@ async function buildStlModels(volume, onProgress = null) {
state.modelBoundsFrame = null; state.modelBoundsFrame = null;
state.modelLocalBounds = null; state.modelLocalBounds = null;
state.stlSignature = signature; state.stlSignature = signature;
const files = state.stlFiles.filter((file) => state.selectedStlIds.has(Number(file.id))); const files = state.stlFiles;
if (!files.length) { if (!files.length) {
applyPose(); applyPose();
resetMappingCanvas("当前没有可见 STL 构件"); resetMappingCanvas("当前没有可见 STL 构件");
@@ -1741,6 +1783,7 @@ async function buildStlModels(volume, onProgress = null) {
}; };
updateBaseModelScaleFromBounds(); updateBaseModelScaleFromBounds();
addModelBoundsFrame(size); addModelBoundsFrame(size);
applyStlVisibility();
applyPose(); applyPose();
scheduleMappingDraw(); scheduleMappingDraw();
onProgress?.(100); onProgress?.(100);
@@ -1768,8 +1811,15 @@ function applyPose(poseInput = state.pose) {
function fitCamera() { function fitCamera() {
if (!state.camera || !state.controls) return; if (!state.camera || !state.controls) return;
const box = new THREE.Box3(); const box = new THREE.Box3();
box.expandByObject(state.dicomGroup); if (state.dicomGroup?.visible !== false) box.expandByObject(state.dicomGroup);
box.expandByObject(state.modelGroup); const modelBox = visibleModelBox();
if (modelBox) box.union(modelBox);
if (box.isEmpty()) {
state.camera.position.set(0, -8.5, 4.4);
state.controls.target.set(0, 0, 0);
state.controls.update();
return;
}
const size = new THREE.Vector3(); const size = new THREE.Vector3();
const center = new THREE.Vector3(); const center = new THREE.Vector3();
box.getSize(size); box.getSize(size);
@@ -1805,13 +1855,13 @@ function resetPose(kind) {
} }
function stretchAxis(axis) { function stretchAxis(axis) {
if (!state.rawModelGroup?.children.length || !state.dicomSceneBox) { const modelBox = visibleModelBox();
if (!modelBox || !state.dicomSceneBox) {
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
return; return;
} }
const axisKey = { x: "scaleX", y: "scaleY", z: "scaleZ" }[axis]; const axisKey = { x: "scaleX", y: "scaleY", z: "scaleZ" }[axis];
if (!axisKey) return; if (!axisKey) return;
const modelBox = new THREE.Box3().setFromObject(state.modelGroup);
const dicomSize = new THREE.Vector3(); const dicomSize = new THREE.Vector3();
const modelSize = new THREE.Vector3(); const modelSize = new THREE.Vector3();
state.dicomSceneBox.getSize(dicomSize); state.dicomSceneBox.getSize(dicomSize);
@@ -1842,7 +1892,7 @@ function suggestBoneSelection() {
state.selectedStlIds = next; state.selectedStlIds = next;
renderStl(); renderStl();
markDirty(); markDirty();
loadFusion(); applyStlSelectionChange();
} }
function readAutoSettings() { function readAutoSettings() {
@@ -1903,12 +1953,13 @@ function restoreAutoPose() {
} }
function candidateScore(pose, settings = null) { function candidateScore(pose, settings = null) {
if (!state.dicomSceneBox || !state.modelGroup || !state.rawModelGroup?.children.length) return -Infinity; if (!state.dicomSceneBox || !state.modelGroup || !visibleModelRecords().length) return -Infinity;
const config = settings || readAutoSettings(); const config = settings || readAutoSettings();
const current = { ...state.pose }; const current = { ...state.pose };
applyPose(pose); applyPose(pose);
const modelBox = new THREE.Box3().setFromObject(state.modelGroup); const modelBox = visibleModelBox();
applyPose(current); applyPose(current);
if (!modelBox) return -Infinity;
const overlap = modelBox.clone().intersect(state.dicomSceneBox); const overlap = modelBox.clone().intersect(state.dicomSceneBox);
const overlapSize = new THREE.Vector3(); const overlapSize = new THREE.Vector3();
overlap.getSize(overlapSize); overlap.getSize(overlapSize);
@@ -1929,7 +1980,7 @@ function candidateScore(pose, settings = null) {
} }
function runAutoCoarse() { function runAutoCoarse() {
if (!state.activeCase || !currentSeries() || !state.rawModelGroup?.children.length) { if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) {
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
return; return;
} }
@@ -1948,7 +1999,7 @@ function runAutoCoarse() {
} }
function runAutoFine() { function runAutoFine() {
if (!state.activeCase || !currentSeries() || !state.rawModelGroup?.children.length) { if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) {
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
return; return;
} }