Improve segmentation mapping render
This commit is contained in:
@@ -104,6 +104,7 @@ const state = {
|
||||
mappingViewport: { scale: 1, offsetX: 0, offsetY: 0, rotation: 0 },
|
||||
mappingDrag: null,
|
||||
mappingDrawFrame: 0,
|
||||
mappingVersion: 0,
|
||||
resizeObserver: null,
|
||||
resizeScene: null,
|
||||
nudgeTimer: null,
|
||||
@@ -122,6 +123,10 @@ function cls(value) {
|
||||
return value ? "active" : "";
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function setTopLoading(visible) {
|
||||
$("topLoading").classList.toggle("hidden", !visible);
|
||||
}
|
||||
@@ -139,6 +144,18 @@ function setFusionLoading(visible, text = "正在构建融合视图", progress =
|
||||
}
|
||||
}
|
||||
|
||||
function setDicomLoading(visible, text = "正在加载分割结果", progress = null) {
|
||||
$("dicomLoading").classList.toggle("hidden", !visible);
|
||||
$("dicomLoading").querySelector("span").textContent = text;
|
||||
const fill = $("dicomProgressFill");
|
||||
const label = $("dicomProgressText");
|
||||
if (!fill || !label) return;
|
||||
const value = progress == null ? 0 : clamp(Number(progress) || 0, 0, 100);
|
||||
fill.style.width = progress == null ? "34%" : `${value}%`;
|
||||
fill.classList.toggle("indeterminate", progress == null);
|
||||
label.textContent = progress == null ? "" : `${Math.round(value)}%`;
|
||||
}
|
||||
|
||||
function setSaveState(text, tone = "") {
|
||||
const el = $("saveState");
|
||||
el.textContent = text;
|
||||
@@ -969,7 +986,7 @@ function updateDicomPreview() {
|
||||
window: state.windowMode,
|
||||
access_token: state.token,
|
||||
});
|
||||
$("dicomLoading").classList.remove("hidden");
|
||||
setDicomLoading(true, "正在加载 DICOM Base Layer", 16);
|
||||
resetMappingCanvas("正在加载 DICOM Base Layer");
|
||||
$("dicomPreview").src = `/api/dicom/image?${params.toString()}`;
|
||||
renderDicomAnnotation();
|
||||
@@ -1370,7 +1387,8 @@ function resetMappingCanvas(message = "当前切片暂无可见构件") {
|
||||
|
||||
function scheduleMappingDraw() {
|
||||
window.cancelAnimationFrame(state.mappingDrawFrame);
|
||||
state.mappingDrawFrame = window.requestAnimationFrame(() => drawMappingView());
|
||||
const version = ++state.mappingVersion;
|
||||
state.mappingDrawFrame = window.requestAnimationFrame(() => drawMappingView(version));
|
||||
}
|
||||
|
||||
function applyMappingTransform() {
|
||||
@@ -1417,82 +1435,209 @@ function addSegmentIntersectionsToRows(rows, width, height, segment) {
|
||||
const a = segment.a;
|
||||
const b = segment.b;
|
||||
if (!Number.isFinite(a.x) || !Number.isFinite(a.y) || !Number.isFinite(b.x) || !Number.isFinite(b.y)) return;
|
||||
const minY = Math.max(0, Math.floor(Math.min(a.y, b.y)));
|
||||
const maxY = Math.min(height - 1, Math.ceil(Math.max(a.y, b.y)));
|
||||
const deltaY = b.y - a.y;
|
||||
if (Math.abs(deltaY) < 1e-6) {
|
||||
const row = Math.max(0, Math.min(height - 1, Math.round(a.y)));
|
||||
rows[row].push(Math.max(0, Math.min(width - 1, a.x)));
|
||||
rows[row].push(Math.max(0, Math.min(width - 1, b.x)));
|
||||
if (Math.abs(deltaY) < 0.01) {
|
||||
return;
|
||||
}
|
||||
const minY = Math.max(0, Math.floor(Math.min(a.y, b.y)));
|
||||
const maxY = Math.min(height - 1, Math.ceil(Math.max(a.y, b.y)));
|
||||
for (let row = minY; row <= maxY; row += 1) {
|
||||
const sampleY = row + 0.5;
|
||||
const crosses = (sampleY >= a.y && sampleY < b.y) || (sampleY >= b.y && sampleY < a.y);
|
||||
if (!crosses) continue;
|
||||
const t = (sampleY - a.y) / deltaY;
|
||||
if (t < 0 || t > 1) continue;
|
||||
const x = a.x + (b.x - a.x) * t;
|
||||
if (Number.isFinite(x)) rows[row].push(x);
|
||||
}
|
||||
}
|
||||
|
||||
function fillSegmentsAsSolidMask(context, width, height, segments, color, opacity = 0.72) {
|
||||
if (!segments.length) return 0;
|
||||
const rgb = parseCssColor(color);
|
||||
const maskCanvas = document.createElement("canvas");
|
||||
maskCanvas.width = width;
|
||||
maskCanvas.height = height;
|
||||
const maskContext = maskCanvas.getContext("2d");
|
||||
const imageData = maskContext.createImageData(width, height);
|
||||
const rows = Array.from({ length: height }, () => []);
|
||||
const alpha = Math.round(Math.max(0.12, Math.min(opacity, 1)) * 190);
|
||||
let filled = 0;
|
||||
|
||||
segments.forEach((segment) => addSegmentIntersectionsToRows(rows, width, height, segment));
|
||||
rows.forEach((intersections, y) => {
|
||||
if (intersections.length < 2) return;
|
||||
intersections.sort((a, b) => a - b);
|
||||
const cleaned = [];
|
||||
intersections.forEach((x) => {
|
||||
const previous = cleaned[cleaned.length - 1];
|
||||
if (previous === undefined || Math.abs(previous - x) > 0.35) cleaned.push(x);
|
||||
function groupPlaneSegmentsByConnectivity(segments, tolerance = 1.35) {
|
||||
if (segments.length <= 1) return segments.length ? [segments] : [];
|
||||
const parents = segments.map((_, index) => index);
|
||||
const find = (index) => {
|
||||
if (parents[index] !== index) parents[index] = find(parents[index]);
|
||||
return parents[index];
|
||||
};
|
||||
const union = (left, right) => {
|
||||
const leftRoot = find(left);
|
||||
const rightRoot = find(right);
|
||||
if (leftRoot !== rightRoot) parents[rightRoot] = leftRoot;
|
||||
};
|
||||
const buckets = new Map();
|
||||
const cellSize = Math.max(tolerance, 0.1);
|
||||
const toleranceSquared = tolerance * tolerance;
|
||||
const cellKey = (x, y) => `${x},${y}`;
|
||||
segments.forEach((segment, index) => {
|
||||
[segment.a, segment.b].forEach((point) => {
|
||||
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return;
|
||||
const cellX = Math.floor(point.x / cellSize);
|
||||
const cellY = Math.floor(point.y / cellSize);
|
||||
for (let dx = -1; dx <= 1; dx += 1) {
|
||||
for (let dy = -1; dy <= 1; dy += 1) {
|
||||
const candidates = buckets.get(cellKey(cellX + dx, cellY + dy));
|
||||
candidates?.forEach((candidate) => {
|
||||
const distanceSquared = (candidate.x - point.x) ** 2 + (candidate.y - point.y) ** 2;
|
||||
if (distanceSquared <= toleranceSquared) union(index, candidate.index);
|
||||
});
|
||||
for (let index = 0; index + 1 < cleaned.length; index += 2) {
|
||||
const startX = Math.max(0, Math.min(width - 1, Math.ceil(cleaned[index])));
|
||||
const endX = Math.max(0, Math.min(width - 1, Math.floor(cleaned[index + 1])));
|
||||
if (endX < startX) continue;
|
||||
for (let x = startX; x <= endX; x += 1) {
|
||||
const offset = (y * width + x) * 4;
|
||||
imageData.data[offset] = rgb.r;
|
||||
imageData.data[offset + 1] = rgb.g;
|
||||
imageData.data[offset + 2] = rgb.b;
|
||||
imageData.data[offset + 3] = alpha;
|
||||
filled += 1;
|
||||
}
|
||||
}
|
||||
const key = cellKey(cellX, cellY);
|
||||
const bucket = buckets.get(key) ?? [];
|
||||
bucket.push({ x: point.x, y: point.y, index });
|
||||
buckets.set(key, bucket);
|
||||
});
|
||||
|
||||
const hullPixels = fillConvexHullMask(context, width, height, segments, color, opacity);
|
||||
maskContext.putImageData(imageData, 0, 0);
|
||||
context.globalAlpha = 0.45;
|
||||
context.drawImage(maskCanvas, 0, 0);
|
||||
context.globalAlpha = 1;
|
||||
return filled + hullPixels;
|
||||
});
|
||||
const groups = new Map();
|
||||
segments.forEach((segment, index) => {
|
||||
const root = find(index);
|
||||
const group = groups.get(root) ?? [];
|
||||
group.push(segment);
|
||||
groups.set(root, group);
|
||||
});
|
||||
return [...groups.values()].sort((left, right) => right.length - left.length);
|
||||
}
|
||||
|
||||
function fillConvexHullMask(context, width, height, segments, color, opacity = 0.72) {
|
||||
const points = [];
|
||||
segments.forEach((segment) => {
|
||||
[segment.a, segment.b].forEach((point) => {
|
||||
if (Number.isFinite(point.x) && Number.isFinite(point.y) && point.x > -width && point.x < width * 2 && point.y > -height && point.y < height * 2) {
|
||||
points.push({ x: point.x, y: point.y });
|
||||
function paintMaskPixel(maskData, width, height, x, y, rgb, alpha) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return 0;
|
||||
const offset = (y * width + x) * 4;
|
||||
if (maskData.data[offset + 3] > 0) return 0;
|
||||
maskData.data[offset] = rgb.r;
|
||||
maskData.data[offset + 1] = rgb.g;
|
||||
maskData.data[offset + 2] = rgb.b;
|
||||
maskData.data[offset + 3] = alpha;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function solidStrokeRadius(width, height) {
|
||||
return Math.max(2.2, Math.min(5.5, Math.max(width, height) * 0.006));
|
||||
}
|
||||
|
||||
function fillSegmentCapsulesIntoMask(maskData, width, height, segments, rgb, alpha, radius) {
|
||||
let paintedPixels = 0;
|
||||
const radiusSquared = radius * radius;
|
||||
segments.forEach(({ a, b }) => {
|
||||
if (!Number.isFinite(a.x) || !Number.isFinite(a.y) || !Number.isFinite(b.x) || !Number.isFinite(b.y)) return;
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const lengthSquared = dx * dx + dy * dy;
|
||||
const minX = clamp(Math.floor(Math.min(a.x, b.x) - radius), 0, width - 1);
|
||||
const maxX = clamp(Math.ceil(Math.max(a.x, b.x) + radius), 0, width - 1);
|
||||
const minY = clamp(Math.floor(Math.min(a.y, b.y) - radius), 0, height - 1);
|
||||
const maxY = clamp(Math.ceil(Math.max(a.y, b.y) + radius), 0, height - 1);
|
||||
for (let y = minY; y <= maxY; y += 1) {
|
||||
for (let x = minX; x <= maxX; x += 1) {
|
||||
const px = x + 0.5;
|
||||
const py = y + 0.5;
|
||||
const t = lengthSquared <= 1e-6
|
||||
? 0
|
||||
: clamp(((px - a.x) * dx + (py - a.y) * dy) / lengthSquared, 0, 1);
|
||||
const closestX = a.x + dx * t;
|
||||
const closestY = a.y + dy * t;
|
||||
if ((px - closestX) ** 2 + (py - closestY) ** 2 <= radiusSquared) {
|
||||
paintedPixels += paintMaskPixel(maskData, width, height, x, y, rgb, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return paintedPixels;
|
||||
}
|
||||
|
||||
function closeSmallMaskGaps(maskData, width, height, rgb, alpha, maxGap = 2) {
|
||||
const toFill = new Set();
|
||||
const hasPixel = (x, y) => maskData.data[(y * width + x) * 4 + 3] > 0;
|
||||
const mark = (x, y) => {
|
||||
if (x >= 0 && x < width && y >= 0 && y < height && !hasPixel(x, y)) toFill.add(y * width + x);
|
||||
};
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
let lastFilled = -1;
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
if (!hasPixel(x, y)) continue;
|
||||
const gap = x - lastFilled - 1;
|
||||
if (lastFilled >= 0 && gap > 0 && gap <= maxGap) {
|
||||
for (let fillX = lastFilled + 1; fillX < x; fillX += 1) mark(fillX, y);
|
||||
}
|
||||
lastFilled = x;
|
||||
}
|
||||
}
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
let lastFilled = -1;
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
if (!hasPixel(x, y)) continue;
|
||||
const gap = y - lastFilled - 1;
|
||||
if (lastFilled >= 0 && gap > 0 && gap <= maxGap) {
|
||||
for (let fillY = lastFilled + 1; fillY < y; fillY += 1) mark(x, fillY);
|
||||
}
|
||||
lastFilled = y;
|
||||
}
|
||||
}
|
||||
toFill.forEach((index) => {
|
||||
const offset = index * 4;
|
||||
maskData.data[offset] = rgb.r;
|
||||
maskData.data[offset + 1] = rgb.g;
|
||||
maskData.data[offset + 2] = rgb.b;
|
||||
maskData.data[offset + 3] = alpha;
|
||||
});
|
||||
return toFill.size;
|
||||
}
|
||||
|
||||
function fillInternalMaskHoles(maskData, width, height, rgb, alpha) {
|
||||
const outside = new Uint8Array(width * height);
|
||||
const stack = [];
|
||||
const pushIfEmpty = (x, y) => {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
||||
const index = y * width + x;
|
||||
if (outside[index] || maskData.data[index * 4 + 3] > 0) return;
|
||||
outside[index] = 1;
|
||||
stack.push(index);
|
||||
};
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
pushIfEmpty(x, 0);
|
||||
pushIfEmpty(x, height - 1);
|
||||
}
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
pushIfEmpty(0, y);
|
||||
pushIfEmpty(width - 1, y);
|
||||
}
|
||||
while (stack.length) {
|
||||
const index = stack.pop();
|
||||
const x = index % width;
|
||||
const y = Math.floor(index / width);
|
||||
pushIfEmpty(x + 1, y);
|
||||
pushIfEmpty(x - 1, y);
|
||||
pushIfEmpty(x, y + 1);
|
||||
pushIfEmpty(x, y - 1);
|
||||
}
|
||||
let patchedPixels = 0;
|
||||
for (let index = 0; index < outside.length; index += 1) {
|
||||
const offset = index * 4;
|
||||
if (!outside[index] && maskData.data[offset + 3] === 0) {
|
||||
maskData.data[offset] = rgb.r;
|
||||
maskData.data[offset + 1] = rgb.g;
|
||||
maskData.data[offset + 2] = rgb.b;
|
||||
maskData.data[offset + 3] = alpha;
|
||||
patchedPixels += 1;
|
||||
}
|
||||
}
|
||||
return patchedPixels;
|
||||
}
|
||||
|
||||
function drawFallbackClosedRegion(context, width, height, segments, color, opacity = 0.72) {
|
||||
const points = segments.flatMap((segment) => [segment.a, segment.b])
|
||||
.filter((point) => (
|
||||
Number.isFinite(point.x)
|
||||
&& Number.isFinite(point.y)
|
||||
&& point.x >= -width
|
||||
&& point.x <= width * 2
|
||||
&& point.y >= -height
|
||||
&& point.y <= height * 2
|
||||
));
|
||||
if (points.length < 3) return 0;
|
||||
const sorted = points
|
||||
.filter((point, index) => points.findIndex((item) => Math.abs(item.x - point.x) < 0.5 && Math.abs(item.y - point.y) < 0.5) === index)
|
||||
.sort((a, b) => (Math.abs(a.x - b.x) > 0.001 ? a.x - b.x : a.y - b.y));
|
||||
if (sorted.length < 3) return 0;
|
||||
const uniquePoints = [];
|
||||
points.forEach((point) => {
|
||||
if (!uniquePoints.some((current) => (current.x - point.x) ** 2 + (current.y - point.y) ** 2 < 1e-6)) uniquePoints.push(point);
|
||||
});
|
||||
if (uniquePoints.length < 3) return 0;
|
||||
const sorted = [...uniquePoints].sort((left, right) => (Math.abs(left.x - right.x) > 1e-6 ? left.x - right.x : left.y - right.y));
|
||||
const cross = (origin, a, b) => (a.x - origin.x) * (b.y - origin.y) - (a.y - origin.y) * (b.x - origin.x);
|
||||
const lower = [];
|
||||
sorted.forEach((point) => {
|
||||
@@ -1505,39 +1650,117 @@ function fillConvexHullMask(context, width, height, segments, color, opacity = 0
|
||||
upper.push(point);
|
||||
});
|
||||
const hull = [...lower.slice(0, -1), ...upper.slice(0, -1)];
|
||||
if (hull.length < 3) return 0;
|
||||
const ordered = hull.length >= 3 ? hull : uniquePoints;
|
||||
context.save();
|
||||
context.globalAlpha = Math.max(0.18, Math.min(opacity, 1)) * 0.58;
|
||||
context.globalAlpha = clamp(opacity, 0.1, 1) * 0.62;
|
||||
context.fillStyle = color;
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = Math.max(1.2, Math.max(width, height) * 0.002);
|
||||
context.beginPath();
|
||||
hull.forEach((point, index) => {
|
||||
ordered.forEach((point, index) => {
|
||||
if (index === 0) context.moveTo(point.x, point.y);
|
||||
else context.lineTo(point.x, point.y);
|
||||
});
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.globalAlpha = Math.max(0.22, Math.min(opacity, 1));
|
||||
context.stroke();
|
||||
context.restore();
|
||||
return Math.max(1, Math.round(hull.length * 6));
|
||||
return Math.max(1, Math.round(ordered.length / 2));
|
||||
}
|
||||
|
||||
function drawMappingView() {
|
||||
function fillSegmentsAsSolidMask(context, width, height, segments, color, opacity = 0.72) {
|
||||
if (!segments.length) return 0;
|
||||
const rgb = parseCssColor(color);
|
||||
const alpha = Math.round(clamp(opacity, 0.1, 1) * 190);
|
||||
const maskCanvas = document.createElement("canvas");
|
||||
maskCanvas.width = width;
|
||||
maskCanvas.height = height;
|
||||
const maskContext = maskCanvas.getContext("2d");
|
||||
if (!maskContext) return 0;
|
||||
const maskData = maskContext.createImageData(width, height);
|
||||
const radius = solidStrokeRadius(width, height);
|
||||
const groups = groupPlaneSegmentsByConnectivity(segments, radius * 1.15);
|
||||
const fallbackGroups = [];
|
||||
let filledPixels = 0;
|
||||
|
||||
groups.forEach((group) => {
|
||||
const rows = Array.from({ length: height }, () => []);
|
||||
group.forEach((segment) => addSegmentIntersectionsToRows(rows, width, height, segment));
|
||||
let groupPixels = 0;
|
||||
rows.forEach((intersections, row) => {
|
||||
if (intersections.length < 2) return;
|
||||
intersections.sort((left, right) => left - right);
|
||||
const cleaned = [];
|
||||
intersections.forEach((x) => {
|
||||
const previous = cleaned[cleaned.length - 1];
|
||||
if (previous === undefined || Math.abs(previous - x) > 0.35) cleaned.push(x);
|
||||
});
|
||||
for (let index = 0; index + 1 < cleaned.length; index += 2) {
|
||||
const rawStartX = cleaned[index];
|
||||
const rawEndX = cleaned[index + 1];
|
||||
if (rawEndX < 0 || rawStartX > width - 1) continue;
|
||||
const startX = clamp(Math.ceil(rawStartX), 0, width - 1);
|
||||
const endX = clamp(Math.floor(rawEndX), 0, width - 1);
|
||||
if (endX < startX) continue;
|
||||
for (let x = startX; x <= endX; x += 1) {
|
||||
const offset = (row * width + x) * 4;
|
||||
if (maskData.data[offset + 3] === 0) {
|
||||
maskData.data[offset] = rgb.r;
|
||||
maskData.data[offset + 1] = rgb.g;
|
||||
maskData.data[offset + 2] = rgb.b;
|
||||
maskData.data[offset + 3] = alpha;
|
||||
groupPixels += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
groupPixels += fillSegmentCapsulesIntoMask(maskData, width, height, group, rgb, alpha, radius);
|
||||
filledPixels += groupPixels;
|
||||
if (groupPixels < Math.max(20, Math.round(group.length * 0.5)) && group.length >= 3) fallbackGroups.push(group);
|
||||
});
|
||||
|
||||
filledPixels += closeSmallMaskGaps(maskData, width, height, rgb, alpha, 3);
|
||||
filledPixels += fillInternalMaskHoles(maskData, width, height, rgb, alpha);
|
||||
maskContext.putImageData(maskData, 0, 0);
|
||||
context.drawImage(maskCanvas, 0, 0);
|
||||
fallbackGroups.forEach((group) => {
|
||||
filledPixels += drawFallbackClosedRegion(context, width, height, group, color, opacity);
|
||||
});
|
||||
|
||||
if (filledPixels === 0) {
|
||||
context.save();
|
||||
context.globalAlpha = clamp(opacity, 0.1, 1) * 0.42;
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = Math.max(0.8, Math.max(width, height) * 0.0012);
|
||||
context.lineCap = "round";
|
||||
context.lineJoin = "round";
|
||||
context.beginPath();
|
||||
segments.forEach((segment) => {
|
||||
context.moveTo(segment.a.x, segment.a.y);
|
||||
context.lineTo(segment.b.x, segment.b.y);
|
||||
});
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
return filledPixels;
|
||||
}
|
||||
|
||||
async function drawMappingView(version = state.mappingVersion) {
|
||||
const image = $("dicomPreview");
|
||||
const canvas = $("mappingCanvas");
|
||||
const volumeScene = state.volumeScene;
|
||||
canvas.style.display = state.mappingMode === "image" ? "none" : "";
|
||||
if (state.mappingMode === "image") {
|
||||
setDicomLoading(false);
|
||||
resetMappingCanvas("单独影像模式未叠加 STL 分割");
|
||||
return;
|
||||
}
|
||||
const visibleRecords = visibleModelRecords();
|
||||
if (!image?.complete || !image.naturalWidth || !volumeScene || !visibleRecords.length) {
|
||||
if (!visibleRecords.length) setDicomLoading(false);
|
||||
else setDicomLoading(true, "等待 DICOM Base Layer", 32);
|
||||
resetMappingCanvas(visibleRecords.length ? "等待 DICOM Base Layer" : "当前没有可见 STL 构件");
|
||||
return;
|
||||
}
|
||||
setDicomLoading(true, "正在计算 STL 分割映射", 48);
|
||||
|
||||
const width = image.naturalWidth;
|
||||
const height = image.naturalHeight;
|
||||
@@ -1561,9 +1784,10 @@ function drawMappingView() {
|
||||
const tempB = new THREE.Vector3();
|
||||
const tempC = new THREE.Vector3();
|
||||
|
||||
visibleRecords.forEach((record, index) => {
|
||||
for (const [index, record] of visibleRecords.entries()) {
|
||||
if (version !== state.mappingVersion) return;
|
||||
const position = record.mesh.geometry.attributes.position;
|
||||
if (!position) return;
|
||||
if (!position) continue;
|
||||
const triangleCount = Math.floor(position.count / 3);
|
||||
const step = Math.max(1, Math.ceil(triangleCount / 70000));
|
||||
const segments = [];
|
||||
@@ -1599,7 +1823,12 @@ function drawMappingView() {
|
||||
pixels,
|
||||
});
|
||||
}
|
||||
});
|
||||
const progress = 48 + ((index + 1) / Math.max(visibleRecords.length, 1)) * 48;
|
||||
const label = record.file.segment_name || record.file.file_name || `STL ${index + 1}`;
|
||||
setDicomLoading(true, `正在计算 ${label}`, progress);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||
}
|
||||
if (version !== state.mappingVersion) return;
|
||||
|
||||
$("mappingStats").textContent = `${activeModules}/${visibleRecords.length} 构件 · ${segmentCount} 边 · ${filledPixels} px`;
|
||||
$("mappingLegend").innerHTML = modules.length
|
||||
@@ -1614,6 +1843,7 @@ function drawMappingView() {
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div class="empty-state">当前切片暂无可见构件</div>`;
|
||||
setDicomLoading(false);
|
||||
}
|
||||
|
||||
async function buildDicomVolume(volume) {
|
||||
@@ -2314,11 +2544,11 @@ function wireEvents() {
|
||||
$("autoIterations").value = 30;
|
||||
});
|
||||
$("dicomPreview").addEventListener("load", () => {
|
||||
$("dicomLoading").classList.add("hidden");
|
||||
setDicomLoading(true, "正在计算 STL 分割映射", 42);
|
||||
scheduleMappingDraw();
|
||||
});
|
||||
$("dicomPreview").addEventListener("error", () => {
|
||||
$("dicomLoading").classList.add("hidden");
|
||||
setDicomLoading(false);
|
||||
resetMappingCanvas("DICOM Base Layer 加载失败");
|
||||
});
|
||||
$("mappingResetBtn").addEventListener("click", () => {
|
||||
|
||||
@@ -353,7 +353,8 @@
|
||||
<div class="dicom-overlay bottom-left" id="dicomInfoBottom">Img: -</div>
|
||||
<div id="dicomLoading" class="fusion-loading hidden">
|
||||
<span>正在加载分割结果</span>
|
||||
<div><i></i></div>
|
||||
<div><i id="dicomProgressFill"></i></div>
|
||||
<em id="dicomProgressText">0%</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mapping-summary">
|
||||
|
||||
Reference in New Issue
Block a user