diff --git a/DICOM_and_UPP配准/static/app.js b/DICOM_and_UPP配准/static/app.js index 6a6eafd..6cad9ff 100644 --- a/DICOM_and_UPP配准/static/app.js +++ b/DICOM_and_UPP配准/static/app.js @@ -8,8 +8,7 @@ const BODY_LABELS = { head_neck: "头颈部", chest: "胸部", upper_abdomen: "上腹部", - lower_abdomen: "下腹部", - pelvis: "盆腔", + abdomen_pelvis: "腹盆部", }; const DEFAULT_POSE = { @@ -40,6 +39,13 @@ const POSE_CONTROLS = [ const STL_COLORS = [0x18d8c9, 0x3f82ff, 0xf5b84b, 0xf87171, 0x9bd76f, 0xd1a6ff, 0x66e0ff, 0xff9f7a]; const CASE_PAGE_SIZE = 20; +const FUSION_BASE_EXTENT = 4.6; +const AXIS_INSET_LENGTH = 17; +const DEFAULT_AXIS_PROJECTION = { + x: { dx: AXIS_INSET_LENGTH, dy: 0, opacity: 0.95 }, + y: { dx: -10, dy: 10, opacity: 0.82 }, + z: { dx: 0, dy: -AXIS_INSET_LENGTH, opacity: 0.95 }, +}; const POSE_SIGNATURE_KEYS = [ "rotateX", "rotateY", @@ -84,8 +90,8 @@ const state = { pose: { ...DEFAULT_POSE }, windowMode: "default", fusionMode: "fusion", - fusionDetail: "low", - modelDetail: "standard", + fusionDetail: "high", + modelDetail: "ultra", mappingMode: "result", sliceIndex: 0, sliceRangeStart: 0, @@ -127,6 +133,7 @@ const state = { mappingVersion: 0, resizeObserver: null, resizeScene: null, + axisProjectionSignature: "", nudgeTimer: null, nudgeDelayTimer: null, }; @@ -147,6 +154,29 @@ function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } +function getDicomDisplaySliceNumber(sliceIndex, totalSlices) { + const total = Math.max(Math.round(Number(totalSlices) || 0), 0); + if (!total) return 0; + return total - clamp(Math.round(Number(sliceIndex) || 0), 0, total - 1); +} + +function getDicomDisplayRange(startIndex, endIndex, totalSlices) { + const first = getDicomDisplaySliceNumber(startIndex, totalSlices); + const second = getDicomDisplaySliceNumber(endIndex, totalSlices); + return { + start: Math.min(first, second), + end: Math.max(first, second), + }; +} + +function setFusionWebglError(message = "") { + const overlay = $("fusionWebglError"); + if (!overlay) return; + overlay.classList.toggle("hidden", !message); + const detail = overlay.querySelector("p"); + if (detail && message) detail.textContent = message; +} + function setTopLoading(visible) { $("topLoading").classList.toggle("hidden", !visible); } @@ -277,9 +307,11 @@ async function bootstrap() { loginVisible(false); try { initScene(); + setFusionWebglError(""); } catch (error) { initGeometryFallback(); $("fusionStatus").textContent = `WebGL 初始化失败:${error.message}`; + setFusionWebglError("三维融合视图暂不可用,请检查浏览器硬件加速、显卡驱动或远程桌面图形支持。二维 DICOM 与映射功能仍可继续使用。"); console.warn(error); } buildPoseControls(); @@ -601,8 +633,8 @@ async function selectCase(ctNumber, algorithmModel = "未指定模型") { const savedTransform = { ...(registration.transform || {}), scaleX: 1, scaleY: 1, scaleZ: 1 }; const keepPreviousPose = switchingModelOnly; state.pose = keepPreviousPose ? { ...DEFAULT_POSE, ...previousPose } : { ...DEFAULT_POSE, ...savedTransform }; - state.fusionDetail = state.fusionDetail || "low"; - state.modelDetail = registration.model_reference?.model_detail || state.modelDetail || "standard"; + state.fusionDetail = registration.model_reference?.fusion_detail || state.fusionDetail || "high"; + state.modelDetail = registration.model_reference?.model_detail || state.modelDetail || "ultra"; state.autoResult = null; state.lastAutoPose = null; state.sliceIndex = 0; @@ -1006,6 +1038,28 @@ function renderPoseControls() { button.classList.toggle("active", Boolean(state.pose[button.dataset.flip])); }); renderActiveHeader(); + updateStretchButtons(); +} + +function isRightAngleRotation(value) { + const normalized = ((Number(value) || 0) % 90 + 90) % 90; + return normalized < 0.001 || Math.abs(normalized - 90) < 0.001; +} + +function canStretchByAxis() { + return ["rotateX", "rotateY", "rotateZ"].every((key) => isRightAngleRotation(state.pose[key])); +} + +function updateStretchButtons() { + const enabled = canStretchByAxis(); + ["x", "y", "z"].forEach((axis) => { + const button = $(`stretch${axis.toUpperCase()}Btn`); + if (!button) return; + button.disabled = !enabled; + button.title = enabled + ? `按 ${axis.toUpperCase()} 方向自动等比例拉伸模型` + : "仅当旋转 X/Y/Z 均为 0 或 90° 的倍数时可用"; + }); } function updateSliceControl() { @@ -1070,14 +1124,17 @@ function updateDicomPreview() { function initScene() { if (state.sceneReady) return; const viewport = $("fusionViewport"); - const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); - renderer.setClearColor(0x000000, 1); + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setClearColor(0x030712, 1); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); + renderer.localClippingEnabled = true; viewport.appendChild(renderer.domElement); const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera(38, 1, 0.01, 1000); - camera.position.set(0, -8.5, 4.4); + scene.background = new THREE.Color(0x030712); + + const camera = new THREE.PerspectiveCamera(45, 1, 0.05, 1000); + camera.position.set(0, -6.2, 4.6); camera.up.set(0, 0, 1); camera.lookAt(0, 0, 0); @@ -1085,21 +1142,22 @@ function initScene() { controls.enabled = false; controls.target.set(0, 0, 0); - const ambient = new THREE.AmbientLight(0xffffff, 0.82); - const directional = new THREE.DirectionalLight(0xffffff, 1.8); - directional.position.set(3.5, -4, 6); - scene.add(ambient, directional); + const ambient = new THREE.AmbientLight(0xffffff, 0.72); + const keyLight = new THREE.DirectionalLight(0xffffff, 1.1); + keyLight.position.set(4, -5, 5); + const fillLight = new THREE.DirectionalLight(0x8fb8ff, 0.55); + fillLight.position.set(-4, 3, 2); + scene.add(ambient, keyLight, fillLight); const dicomGroup = new THREE.Group(); const modelGroup = new THREE.Group(); const rawModelGroup = new THREE.Group(); - const axisGroup = createSceneAxes(); const fusionRoot = new THREE.Group(); modelGroup.add(rawModelGroup); - fusionRoot.add(dicomGroup, modelGroup, axisGroup); + fusionRoot.add(dicomGroup, modelGroup); scene.add(fusionRoot); - Object.assign(state, { renderer, camera, scene, controls, fusionRoot, dicomGroup, modelGroup, rawModelGroup, axisGroup, sceneReady: true }); + Object.assign(state, { renderer, camera, scene, controls, fusionRoot, dicomGroup, modelGroup, rawModelGroup, axisGroup: null, sceneReady: true }); const resize = () => { const rect = viewport.getBoundingClientRect(); @@ -1116,10 +1174,81 @@ function initScene() { const animate = () => { requestAnimationFrame(animate); applySceneViewPose(); + updateFusionAxisInset(); renderer.render(scene, camera); }; animate(); wireFusionSceneDrag(renderer.domElement); + updateFusionAxisInset(DEFAULT_AXIS_PROJECTION); +} + +function axisProjectionSignature(projection) { + return ["x", "y", "z"] + .map((key) => { + const item = projection[key] || DEFAULT_AXIS_PROJECTION[key]; + return `${Math.round(item.dx * 10)},${Math.round(item.dy * 10)},${Math.round(item.opacity * 100)}`; + }) + .join("|"); +} + +function projectModelAxisDirections(camera, object) { + const origin = object.getWorldPosition(new THREE.Vector3()); + const originProjected = origin.clone().project(camera); + const quaternion = object.getWorldQuaternion(new THREE.Quaternion()); + const axisDirections = { + x: new THREE.Vector3(1, 0, 0), + y: new THREE.Vector3(0, 1, 0), + z: new THREE.Vector3(0, 0, 1), + }; + const projectAxis = (direction) => { + const end = origin.clone().add(direction.applyQuaternion(quaternion).normalize().multiplyScalar(0.72)); + const endProjected = end.project(camera); + const dx = endProjected.x - originProjected.x; + const dy = originProjected.y - endProjected.y; + const magnitude = Math.hypot(dx, dy); + if (magnitude < 0.0001) return { dx: 0, dy: -5, opacity: 0.5 }; + return { + dx: (dx / magnitude) * AXIS_INSET_LENGTH, + dy: (dy / magnitude) * AXIS_INSET_LENGTH, + opacity: endProjected.z < originProjected.z ? 1 : 0.58, + }; + }; + return { + x: projectAxis(axisDirections.x), + y: projectAxis(axisDirections.y), + z: projectAxis(axisDirections.z), + }; +} + +function updateFusionAxisInset(projection = null) { + if (!projection) { + if (!state.camera || !state.modelGroup) projection = DEFAULT_AXIS_PROJECTION; + else { + state.fusionRoot?.updateMatrixWorld(true); + state.modelGroup.updateMatrixWorld(true); + projection = projectModelAxisDirections(state.camera, state.modelGroup); + } + } + const signature = axisProjectionSignature(projection); + if (state.axisProjectionSignature === signature) return; + state.axisProjectionSignature = signature; + const origin = { x: 25, y: 31 }; + const items = { + x: { line: $("fusionAxisXLine"), text: $("fusionAxisXText"), group: $("fusionAxisX") }, + y: { line: $("fusionAxisYLine"), text: $("fusionAxisYText"), group: $("fusionAxisY") }, + z: { line: $("fusionAxisZLine"), text: $("fusionAxisZText"), group: $("fusionAxisZ") }, + }; + Object.entries(items).forEach(([key, item]) => { + const vector = projection[key] || DEFAULT_AXIS_PROJECTION[key]; + const endX = origin.x + vector.dx; + const endY = origin.y + vector.dy; + item.group?.setAttribute("opacity", String(vector.opacity)); + item.line?.setAttribute("x2", String(endX)); + item.line?.setAttribute("y2", String(endY)); + item.text?.setAttribute("x", String(endX + (vector.dx >= 0 ? 4 : -4))); + item.text?.setAttribute("y", String(endY + (vector.dy >= 0 ? 6 : -3))); + item.text?.setAttribute("text-anchor", vector.dx >= 0 ? "start" : "end"); + }); } function createAxisLabel(text, color) { @@ -1168,7 +1297,7 @@ function initGeometryFallback() { const rawModelGroup = new THREE.Group(); modelGroup.add(rawModelGroup); fusionRoot.add(dicomGroup, modelGroup); - Object.assign(state, { fusionRoot, dicomGroup, modelGroup, rawModelGroup }); + Object.assign(state, { fusionRoot, dicomGroup, modelGroup, rawModelGroup, axisGroup: null }); } function applySceneViewPose() { @@ -1257,6 +1386,7 @@ function clearFusion() { state.modelLocalBounds = null; $("fusionMeta").textContent = "DICOM - · STL -"; $("fusionStatus").textContent = "等待选择 DICOM 与 STL"; + updateFusionAxisInset(DEFAULT_AXIS_PROJECTION); updateSliceControl(); resetMappingCanvas(); } @@ -1274,17 +1404,20 @@ function updateFusionMeta() { return; } const volume = state.volumeMeta; - $("fusionMeta").textContent = volume - ? `DICOM ${volume.start + 1}-${volume.end + 1}/${volume.total} · STL ${selectedCount} 个` - : `DICOM - · STL ${selectedCount} 个`; + if (!volume) { + $("fusionMeta").textContent = `DICOM - · STL ${selectedCount} 个`; + return; + } + const displayRange = getDicomDisplayRange(volume.start, volume.end, volume.total); + $("fusionMeta").textContent = `DICOM ${displayRange.start}-${displayRange.end}/${volume.total} · STL ${selectedCount} 个`; } function fusionDetailSettings() { return { - low: { planeOpacity: 1, sliceOpacity: 0.045, slicePlanes: 24, pointOpacity: 0, pointSize: 0.012, pointGrid: 72, pointThreshold: 36, textureSize: 384 }, - medium: { planeOpacity: 1, sliceOpacity: 0.07, slicePlanes: 48, pointOpacity: 0, pointSize: 0.014, pointGrid: 88, pointThreshold: 34, textureSize: 640 }, - high: { planeOpacity: 1, sliceOpacity: 0.11, slicePlanes: 72, pointOpacity: 0, pointSize: 0.015, pointGrid: 104, pointThreshold: 32, textureSize: 1024 }, - }[state.fusionDetail] || { planeOpacity: 1, sliceOpacity: 0.045, slicePlanes: 24, pointOpacity: 0, pointSize: 0.012, pointGrid: 72, pointThreshold: 36, textureSize: 384 }; + low: { planeOpacity: 0.42, sliceOpacity: 0.045, boxOpacity: 0.2, slicePlanes: 24, pointOpacity: 0, pointSize: 0.012, pointGrid: 72, pointThreshold: 36, textureSize: 384 }, + medium: { planeOpacity: 0.56, sliceOpacity: 0.075, boxOpacity: 0.26, slicePlanes: 48, pointOpacity: 0, pointSize: 0.014, pointGrid: 88, pointThreshold: 34, textureSize: 640 }, + high: { planeOpacity: 0.68, sliceOpacity: 0.11, boxOpacity: 0.32, slicePlanes: 72, pointOpacity: 0, pointSize: 0.015, pointGrid: 104, pointThreshold: 32, textureSize: 1024 }, + }[state.fusionDetail] || { planeOpacity: 0.42, sliceOpacity: 0.045, boxOpacity: 0.2, slicePlanes: 24, pointOpacity: 0, pointSize: 0.012, pointGrid: 72, pointThreshold: 36, textureSize: 384 }; } function modelDetailSettings() { @@ -1311,7 +1444,7 @@ function applyFusionDetail() { } state.dicomSlicePlanes.forEach((plane) => { if (!plane.material) return; - plane.material.opacity = detail.sliceOpacity; + plane.material.opacity = plane.userData?.rangeBoundary ? detail.planeOpacity : detail.sliceOpacity; plane.material.needsUpdate = true; }); } @@ -1358,6 +1491,7 @@ async function loadFusion() { clearFusion(); return; } + $("fusionStatus").textContent = "正在构建三维融合场景..."; setFusionLoading(true, "正在读取 DICOM 切片范围", 8); updateSliceControl(); try { @@ -1415,6 +1549,38 @@ async function loadTexture(url) { }); } +async function loadDicomFusionTexture(url) { + const image = await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = url; + }); + const width = image.naturalWidth || image.width || 1; + const height = image.naturalHeight || image.height || 1; + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext("2d", { willReadFrequently: true }); + context.drawImage(image, 0, 0, width, height); + const imageData = context.getImageData(0, 0, width, height); + for (let index = 0; index < imageData.data.length; index += 4) { + const value = imageData.data[index]; + imageData.data[index + 1] = value; + imageData.data[index + 2] = value; + imageData.data[index + 3] = value > 4 ? Math.min(235, 42 + Math.round(value * 0.76)) : 0; + } + context.putImageData(imageData, 0, 0); + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + texture.generateMipmaps = false; + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.anisotropy = Math.min(4, state.renderer?.capabilities?.getMaxAnisotropy?.() || 1); + texture.needsUpdate = true; + return texture; +} + async function loadImageData(url, maxSize = 256) { const image = await new Promise((resolve, reject) => { const img = new Image(); @@ -1932,7 +2098,7 @@ async function buildDicomVolume(volume) { state.dicomSlicePlanes = []; const physical = volume.physicalSize || { width: 1, height: 1, depth: 1 }; const maxPhysical = Math.max(physical.width || 1, physical.height || 1, physical.depth || 1, 1); - const sceneScale = 4.8 / maxPhysical; + const sceneScale = FUSION_BASE_EXTENT / maxPhysical; const width = (physical.width || 1) * sceneScale; const height = (physical.height || 1) * sceneScale; const depth = Math.max((physical.depth || 1) * sceneScale, 0.18); @@ -1940,13 +2106,10 @@ async function buildDicomVolume(volume) { const total = Math.max(1, Number(volume.total || 1)); const indices = volume.indices || []; const frames = volume.frames || []; - const centerPosition = indices.length - ? indices.reduce((bestIndex, item, index) => { - const bestDistance = Math.abs(Number(indices[bestIndex] ?? centerIndex) - centerIndex); - const distance = Math.abs(Number(item ?? centerIndex) - centerIndex); - return distance < bestDistance ? index : bestIndex; - }, 0) - : 0; + const fusionDetail = fusionDetailSettings(); + const rangeStart = clamp(Number(volume.start ?? indices[0] ?? centerIndex) || 0, 0, total - 1); + const rangeEnd = clamp(Number(volume.end ?? indices[indices.length - 1] ?? centerIndex) || 0, 0, total - 1); + const boundaryIndices = new Set([Math.min(rangeStart, rangeEnd), Math.max(rangeStart, rangeEnd)]); state.volumeScene = { width, height, @@ -1960,7 +2123,7 @@ async function buildDicomVolume(volume) { const boxMesh = new THREE.Mesh( new THREE.BoxGeometry(width, height, depth), - new THREE.MeshBasicMaterial({ color: 0x020617, transparent: true, opacity: 0.045, depthWrite: false }), + new THREE.MeshBasicMaterial({ color: 0x020617, transparent: true, opacity: fusionDetail.boxOpacity, depthWrite: false }), ); state.dicomGroup.add(boxMesh); const edges = new THREE.LineSegments( @@ -1969,58 +2132,28 @@ async function buildDicomVolume(volume) { ); state.dicomGroup.add(edges); - const localStart = Number(volume.start ?? indices[0] ?? centerIndex); - const localEnd = Number(volume.end ?? indices[indices.length - 1] ?? centerIndex); - const rangeDepth = Math.max(0.015, Math.abs(sliceToSceneZ(localEnd) - sliceToSceneZ(localStart))); - const rangeBox = new THREE.LineSegments( - new THREE.EdgesGeometry(new THREE.BoxGeometry(width * 0.94, height * 0.94, rangeDepth)), - new THREE.LineBasicMaterial({ color: 0x38bdf8, transparent: true, opacity: 0.24 }), - ); - rangeBox.position.z = (sliceToSceneZ(localStart) + sliceToSceneZ(localEnd)) / 2; - state.dicomGroup.add(rangeBox); - - const centerFrame = (volume.frames || [])[centerPosition]; - const fusionDetail = fusionDetailSettings(); - if (centerFrame) { - const texture = await loadTexture(centerFrame); + const planeGeometry = new THREE.PlaneGeometry(width, height); + for (let frameIndex = 0; frameIndex < frames.length; frameIndex += 1) { + const dicomIndex = clamp(Number(indices[frameIndex] ?? centerIndex) || 0, 0, total - 1); + const isBoundary = boundaryIndices.has(dicomIndex) || frames.length === 1; + const texture = await loadDicomFusionTexture(frames[frameIndex]); const plane = new THREE.Mesh( - new THREE.PlaneGeometry(width, height), + planeGeometry, new THREE.MeshBasicMaterial({ map: texture, transparent: true, - opacity: fusionDetail.planeOpacity, + opacity: isBoundary ? fusionDetail.planeOpacity : fusionDetail.sliceOpacity, side: THREE.DoubleSide, depthWrite: false, - depthTest: false, + depthTest: true, }), ); - plane.position.z = sliceToSceneZ(centerIndex) + 0.006; - plane.renderOrder = 80; + plane.position.z = sliceToSceneZ(dicomIndex) + (isBoundary ? 0.006 : 0); + plane.renderOrder = isBoundary ? 16 : 4; + plane.userData.rangeBoundary = isBoundary; state.dicomGroup.add(plane); - state.dicomPlane = plane; - } - - if (frames.length > 1) { - const planeStep = Math.max(1, Math.ceil(frames.length / Math.max(1, fusionDetail.slicePlanes))); - for (let frameIndex = 0; frameIndex < frames.length; frameIndex += planeStep) { - if (frameIndex === centerPosition) continue; - const texture = await loadTexture(frames[frameIndex]); - const plane = new THREE.Mesh( - new THREE.PlaneGeometry(width, height), - new THREE.MeshBasicMaterial({ - map: texture, - transparent: true, - opacity: fusionDetail.sliceOpacity, - side: THREE.DoubleSide, - depthWrite: false, - depthTest: true, - }), - ); - plane.position.z = sliceToSceneZ(Number(indices[frameIndex] ?? centerIndex)); - plane.renderOrder = 4; - state.dicomGroup.add(plane); - state.dicomSlicePlanes.push(plane); - } + state.dicomSlicePlanes.push(plane); + if (isBoundary && !state.dicomPlane) state.dicomPlane = plane; } if (fusionDetail.pointOpacity > 0) { @@ -2076,7 +2209,7 @@ function updateBaseModelScaleFromBounds() { const maxModelSize = Math.max(Number(size.x) || 0, Number(size.y) || 0, Number(size.z) || 0, 1); const volumeSize = state.volumeScene ? Math.max(state.volumeScene.width, state.volumeScene.height, state.volumeScene.depth, 1) - : 4.8; + : FUSION_BASE_EXTENT; state.baseModelScale = (volumeSize / maxModelSize) * 0.92; } @@ -2088,10 +2221,7 @@ function addModelBoundsFrame(size) { new THREE.LineBasicMaterial({ color: 0xfacc15, transparent: true, - opacity: 0.96, - depthTest: false, - depthWrite: false, - toneMapped: false, + opacity: 0.72, }), ); frame.name = "model-bounds"; @@ -2244,41 +2374,19 @@ function applyPose(poseInput = state.pose) { function fitCamera() { if (!state.camera) return; applySceneViewPose(); - const box = new THREE.Box3(); - 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.camera.lookAt(0, 0, 0); - state.controls?.target.set(0, 0, 0); - state.controls?.update(); - return; - } - const size = new THREE.Vector3(); - const center = new THREE.Vector3(); - box.getSize(size); - box.getCenter(center); - const distance = Math.max(size.x, size.y, size.z, 1) * 1.8; - state.camera.position.set(center.x, center.y - distance, center.z + distance * 0.45); - state.camera.lookAt(center); - state.controls?.target.copy(center); - if (state.axisGroup) { - const axisScale = Math.max(size.x, size.y, size.z, 1) / 5.5; - state.axisGroup.scale.setScalar(axisScale); - state.axisGroup.position.set( - center.x - size.x * 0.46, - center.y - size.y * 0.46, - center.z - size.z * 0.42, - ); - } + state.camera.position.set(0, -6.2, 4.6); + state.camera.up.set(0, 0, 1); + state.camera.lookAt(0, 0, 0); + state.controls?.target.set(0, 0, 0); state.controls?.update(); + updateFusionAxisInset(DEFAULT_AXIS_PROJECTION); } function resetSceneView() { state.viewPose = { rotateX: 1.012, rotateZ: -0.314, translateX: 0, translateY: 0, scale: 1 }; applySceneViewPose(); fitCamera(); + $("fusionStatus").textContent = state.volumeMeta ? "三维融合视角已复位" : "等待选择 DICOM 与 STL"; } function resetPose(kind) { @@ -2297,6 +2405,13 @@ function resetPose(kind) { } function stretchAxis(axis) { + if (!canStretchByAxis()) { + const message = "仅当旋转 X/Y/Z 均为 0 或 90° 的倍数时可使用 XYZ 拉伸。"; + setAutoState("XYZ 拉伸不可用", "warn"); + $("autoResult").textContent = message; + updateStretchButtons(); + return; + } const modelBox = visibleModelBox(); if (!modelBox || !state.dicomSceneBox) { $("autoResult").textContent = "请先选择 DICOM 序列和至少一个 STL。"; diff --git a/DICOM_and_UPP配准/static/index.html b/DICOM_and_UPP配准/static/index.html index 28dc23f..c0d1e0f 100644 --- a/DICOM_and_UPP配准/static/index.html +++ b/DICOM_and_UPP配准/static/index.html @@ -119,8 +119,7 @@ - - +
@@ -163,9 +162,9 @@
- + - +
@@ -283,28 +282,56 @@ -
-
-
等待选择 DICOM 与 STL
-
DICOM - · STL -
-