2026-05-24-18-59-49 修正薄壳构件实体化映射

This commit is contained in:
2026-05-24 19:10:02 +08:00
parent 1dcfc2a4c1
commit 21b04ecffd
8 changed files with 337 additions and 20 deletions

View File

@@ -70,7 +70,7 @@ export const displayOptions: Array<{ id: DisplayLevel; label: string; limit: num
{ id: 'standard', label: '标准', limit: 16000 },
{ id: 'fine', label: '精细', limit: 36000 },
{ id: 'ultra', label: '超精细', limit: 72000 },
{ id: 'solid', label: '实体', limit: 200000 },
{ id: 'solid', label: '实体', limit: 800000 },
];
export const dicomOpacityOptions: Array<{ id: DicomOpacityLevel; label: string; sliceOpacity: number; volumeOpacity: number; boxOpacity: number }> = [
{ id: 'low', label: '低', sliceOpacity: 0.82, volumeOpacity: 0.12, boxOpacity: 0.32 },
@@ -707,13 +707,14 @@ export function FusionThreeView({
const stlFiles = project.stlFiles ?? [];
const visibleStlFiles = stlFiles.filter((fileName) => moduleStyles[fileName]?.visible !== false);
const modelPreviewLimit = solidMode ? Math.max(detailLimit, 800000) : detailLimit;
let modelBaseScale = 1;
let loadedModels = 0;
let failedModels = 0;
const loadedBounds: Array<{ min: THREE.Vector3; max: THREE.Vector3 }> = [];
Promise.allSettled(stlFiles.map((fileName, index) => (
getCachedModelPreview(project.id, fileName, detailLimit)
getCachedModelPreview(project.id, fileName, modelPreviewLimit)
.then((payload) => {
if (disposed) return;
const style = moduleStyles[fileName] ?? {
@@ -732,11 +733,12 @@ export function FusionThreeView({
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(payload.vertices, 3));
geometry.computeVertexNormals();
const materialOpacity = solidMode ? Math.max(style.opacity, 0.94) : style.opacity;
const materialOpacity = solidMode ? 1 : style.opacity;
const material = new THREE.MeshStandardMaterial({
color: style.color,
transparent: true,
transparent: materialOpacity < 1,
opacity: materialOpacity,
depthWrite: materialOpacity >= 1,
roughness: solidMode ? 0.56 : 0.48,
metalness: 0.03,
side: THREE.DoubleSide,
@@ -1816,6 +1818,80 @@ function closeSmallMaskGaps(
return toFill.size;
}
function solidStrokeRadius(width: number, height: number) {
return Math.max(2.2, Math.min(5.5, Math.max(width, height) * 0.006));
}
function paintMaskPixel(
maskData: ImageData,
width: number,
height: number,
x: number,
y: number,
rgb: { r: number; g: number; b: number },
alpha: number,
) {
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 fillSegmentCapsulesIntoMask(
maskData: ImageData,
width: number,
height: number,
segments: PlaneSegment[],
rgb: { r: number; g: number; b: number },
alpha: number,
radius: number,
) {
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;
const distanceSquared = (px - closestX) ** 2 + (py - closestY) ** 2;
if (distanceSquared <= radiusSquared) {
paintedPixels += paintMaskPixel(maskData, width, height, x, y, rgb, alpha);
}
}
}
});
return paintedPixels;
}
function drawFallbackClosedRegion(
context: CanvasRenderingContext2D,
width: number,
@@ -1912,7 +1988,8 @@ function fillSegmentsAsSolidMask(
}
const maskData = maskContext.createImageData(width, height);
let filledPixels = 0;
const groups = groupPlaneSegmentsByConnectivity(segments);
const radius = solidStrokeRadius(width, height);
const groups = groupPlaneSegmentsByConnectivity(segments, radius * 1.15);
const fallbackGroups: PlaneSegment[][] = [];
groups.forEach((group) => {
@@ -1959,14 +2036,15 @@ function fillSegmentsAsSolidMask(
}
}
});
groupPixels += fillSegmentCapsulesIntoMask(maskData, width, height, group, rgb, alpha, radius);
filledPixels += groupPixels;
if (groupPixels < Math.max(12, Math.round(group.length * 0.45)) && group.length >= 3) {
if (groupPixels < Math.max(20, Math.round(group.length * 0.5)) && group.length >= 3) {
fallbackGroups.push(group);
}
});
filledPixels += closeSmallMaskGaps(maskData, width, height, rgb, alpha);
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);
@@ -2221,7 +2299,7 @@ export function VoxelizationMappingView({
let disposed = false;
let loaded = 0;
const total = visibleStlFiles.length;
const previewLimit = Math.max(detailLimit, 500000);
const previewLimit = Math.max(detailLimit, 800000);
const updateLoadProgress = (phase: string) => {
if (!disposed) {
setOverlayLoadState({ loading: true, loaded, total, phase });
@@ -3396,7 +3474,7 @@ export default function ReverseWorkspace({
const fusionStart = Math.min(displayStart, displayEnd);
const fusionEnd = Math.max(displayStart, displayEnd);
const previewLimit = selectedDisplay.limit;
const mappingPreviewLimit = Math.max(previewLimit, 200000);
const mappingPreviewLimit = Math.max(previewLimit, 800000);
const total = 2 + stlFilesForLoad.length * 2;
const startedAt = Date.now();
let loaded = 0;