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 [
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();
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) => `
<div class="legend-item" title="${escapeHtml(item.name)}">
@@ -1667,9 +1708,10 @@ async function buildStlModels(volume, onProgress = null) {
const signature = stlSelectionSignature();
if (signature && signature === state.stlSignature && state.rawModelGroup?.children.length) {
updateBaseModelScaleFromBounds();
if (!state.modelBoundsFrame && state.modelLocalBounds?.size) addModelBoundsFrame(state.modelLocalBounds.size);
applyStlVisibility();
applyPose();
applyModelDetail();
if (!state.modelBoundsFrame && state.modelLocalBounds?.size) addModelBoundsFrame(state.modelLocalBounds.size);
scheduleMappingDraw();
onProgress?.(100);
return;
@@ -1682,7 +1724,7 @@ async function buildStlModels(volume, onProgress = null) {
state.modelBoundsFrame = null;
state.modelLocalBounds = null;
state.stlSignature = signature;
const files = state.stlFiles.filter((file) => state.selectedStlIds.has(Number(file.id)));
const files = state.stlFiles;
if (!files.length) {
applyPose();
resetMappingCanvas("当前没有可见 STL 构件");
@@ -1741,6 +1783,7 @@ async function buildStlModels(volume, onProgress = null) {
};
updateBaseModelScaleFromBounds();
addModelBoundsFrame(size);
applyStlVisibility();
applyPose();
scheduleMappingDraw();
onProgress?.(100);
@@ -1768,8 +1811,15 @@ function applyPose(poseInput = state.pose) {
function fitCamera() {
if (!state.camera || !state.controls) return;
const box = new THREE.Box3();
box.expandByObject(state.dicomGroup);
box.expandByObject(state.modelGroup);
if (state.dicomGroup?.visible !== false) box.expandByObject(state.dicomGroup);
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 center = new THREE.Vector3();
box.getSize(size);
@@ -1805,13 +1855,13 @@ function resetPose(kind) {
}
function stretchAxis(axis) {
if (!state.rawModelGroup?.children.length || !state.dicomSceneBox) {
const modelBox = visibleModelBox();
if (!modelBox || !state.dicomSceneBox) {
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
return;
}
const axisKey = { x: "scaleX", y: "scaleY", z: "scaleZ" }[axis];
if (!axisKey) return;
const modelBox = new THREE.Box3().setFromObject(state.modelGroup);
const dicomSize = new THREE.Vector3();
const modelSize = new THREE.Vector3();
state.dicomSceneBox.getSize(dicomSize);
@@ -1842,7 +1892,7 @@ function suggestBoneSelection() {
state.selectedStlIds = next;
renderStl();
markDirty();
loadFusion();
applyStlSelectionChange();
}
function readAutoSettings() {
@@ -1903,12 +1953,13 @@ function restoreAutoPose() {
}
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 current = { ...state.pose };
applyPose(pose);
const modelBox = new THREE.Box3().setFromObject(state.modelGroup);
const modelBox = visibleModelBox();
applyPose(current);
if (!modelBox) return -Infinity;
const overlap = modelBox.clone().intersect(state.dicomSceneBox);
const overlapSize = new THREE.Vector3();
overlap.getSize(overlapSize);
@@ -1929,7 +1980,7 @@ function candidateScore(pose, settings = null) {
}
function runAutoCoarse() {
if (!state.activeCase || !currentSeries() || !state.rawModelGroup?.children.length) {
if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) {
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
return;
}
@@ -1948,7 +1999,7 @@ function runAutoCoarse() {
}
function runAutoFine() {
if (!state.activeCase || !currentSeries() || !state.rawModelGroup?.children.length) {
if (!state.activeCase || !currentSeries() || !visibleModelRecords().length) {
$("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。";
return;
}