Cache STL workspace visibility toggles
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user