2026-05-07-16-53-23 修正3D模型中心和坐标轴
This commit is contained in:
@@ -49,6 +49,10 @@ interface ModelPreviewPayload {
|
||||
triangleCount: number;
|
||||
sampledTriangles: number;
|
||||
vertices: number[];
|
||||
bounds?: {
|
||||
min: { x: number; y: number; z: number };
|
||||
max: { x: number; y: number; z: number };
|
||||
};
|
||||
}
|
||||
|
||||
type ModelPoseKey = keyof ModelPose;
|
||||
@@ -235,6 +239,53 @@ function DicomCanvas({ preview, rotation }: { preview: DicomPreview; rotation: n
|
||||
);
|
||||
}
|
||||
|
||||
function OrientationGizmo({ pose }: { pose: ModelPose }) {
|
||||
const axes = useMemo(() => {
|
||||
const rotation = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(
|
||||
THREE.MathUtils.degToRad(pose.rotateX),
|
||||
THREE.MathUtils.degToRad(pose.rotateY),
|
||||
THREE.MathUtils.degToRad(pose.rotateZ),
|
||||
'XYZ',
|
||||
));
|
||||
return [
|
||||
{ id: 'X', color: '#ef4444', vector: new THREE.Vector3(1, 0, 0).applyMatrix4(rotation) },
|
||||
{ id: 'Y', color: '#10b981', vector: new THREE.Vector3(0, 1, 0).applyMatrix4(rotation) },
|
||||
{ id: 'Z', color: '#3b82f6', vector: new THREE.Vector3(0, 0, 1).applyMatrix4(rotation) },
|
||||
]
|
||||
.map((axis) => ({
|
||||
...axis,
|
||||
endX: 38 + axis.vector.x * 24 + axis.vector.z * 10,
|
||||
endY: 38 - axis.vector.y * 24 + axis.vector.z * 8,
|
||||
labelX: 38 + axis.vector.x * 30 + axis.vector.z * 12,
|
||||
labelY: 38 - axis.vector.y * 30 + axis.vector.z * 10,
|
||||
opacity: 0.55 + Math.max(-axis.vector.z, 0) * 0.45,
|
||||
}))
|
||||
.sort((a, b) => b.vector.z - a.vector.z);
|
||||
}, [pose.rotateX, pose.rotateY, pose.rotateZ]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute bottom-4 right-4 rounded-xl border border-slate-200 bg-white/90 px-3 py-2 shadow-sm">
|
||||
<svg width="76" height="70" viewBox="0 0 76 70" className="mb-1 block">
|
||||
<circle cx="38" cy="38" r="2.5" fill="#0f172a" opacity="0.28" />
|
||||
{axes.map((axis) => (
|
||||
<g key={axis.id} opacity={axis.opacity}>
|
||||
<line x1="38" y1="38" x2={axis.endX} y2={axis.endY} stroke={axis.color} strokeWidth="2.2" strokeLinecap="round" />
|
||||
<circle cx={axis.endX} cy={axis.endY} r="2.4" fill={axis.color} />
|
||||
<text x={axis.labelX} y={axis.labelY} fill={axis.color} fontSize="10" fontWeight="900" textAnchor="middle" dominantBaseline="middle">
|
||||
{axis.id}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
<div className="space-y-0.5 font-mono text-[9px] text-slate-500">
|
||||
<div>X {Math.round(pose.rotateX)}°</div>
|
||||
<div>Y {Math.round(pose.rotateY)}°</div>
|
||||
<div>Z {Math.round(pose.rotateZ)}°</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NativeStlViewer({
|
||||
projectId,
|
||||
files,
|
||||
@@ -424,6 +475,7 @@ function NativeStlViewer({
|
||||
scene.add(poseGroup);
|
||||
let loaded = 0;
|
||||
let failed = 0;
|
||||
const loadedBounds: Array<{ min: THREE.Vector3; max: THREE.Vector3 }> = [];
|
||||
|
||||
visibleFiles.forEach((fileName) => {
|
||||
fetch(`/api/projects/${projectId}/models/${encodeURIComponent(fileName)}/preview?limit=${detailLimit}`)
|
||||
@@ -451,18 +503,42 @@ function NativeStlViewer({
|
||||
}),
|
||||
);
|
||||
pivotGroup.add(mesh);
|
||||
if (payload.bounds) {
|
||||
loadedBounds.push({
|
||||
min: new THREE.Vector3(payload.bounds.min.x, payload.bounds.min.y, payload.bounds.min.z),
|
||||
max: new THREE.Vector3(payload.bounds.max.x, payload.bounds.max.y, payload.bounds.max.z),
|
||||
});
|
||||
} else {
|
||||
geometry.computeBoundingBox();
|
||||
const geometryBox = geometry.boundingBox;
|
||||
if (geometryBox) {
|
||||
loadedBounds.push({
|
||||
min: geometryBox.min.clone(),
|
||||
max: geometryBox.max.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
loaded += 1;
|
||||
setProgress(Math.round(((loaded + failed) / visibleFiles.length) * 100));
|
||||
setStatus(`已加载 ${loaded} / ${visibleFiles.length} 个 STL 预览`);
|
||||
|
||||
if (loaded + failed === visibleFiles.length) {
|
||||
const box = new THREE.Box3().setFromObject(pivotGroup);
|
||||
const box = new THREE.Box3();
|
||||
if (loadedBounds.length) {
|
||||
loadedBounds.forEach((bounds) => {
|
||||
box.expandByPoint(bounds.min);
|
||||
box.expandByPoint(bounds.max);
|
||||
});
|
||||
} else {
|
||||
box.setFromObject(pivotGroup);
|
||||
}
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const maxSize = Math.max(size.x, size.y, size.z) || 1;
|
||||
pivotGroup.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
object.geometry.translate(-center.x, -center.y, -center.z);
|
||||
object.geometry.computeBoundingBox();
|
||||
object.geometry.computeBoundingSphere();
|
||||
object.geometry.computeVertexNormals();
|
||||
}
|
||||
@@ -545,23 +621,7 @@ function NativeStlViewer({
|
||||
{status}
|
||||
</div>
|
||||
)}
|
||||
<div className="pointer-events-none absolute bottom-4 right-4 rounded-xl border border-slate-200 bg-white/85 px-3 py-2 shadow-sm">
|
||||
<div className="mb-2 flex h-12 w-16 items-end justify-center">
|
||||
<div className="relative h-10 w-10">
|
||||
<span className="absolute left-5 top-5 h-px w-8 origin-left -rotate-[18deg] bg-red-500" />
|
||||
<span className="absolute left-[19px] top-5 h-8 w-px origin-bottom bg-emerald-500" />
|
||||
<span className="absolute left-5 top-5 h-px w-7 origin-left rotate-[42deg] bg-blue-500" />
|
||||
<span className="absolute -right-3 top-3 text-[9px] font-black text-red-500">X</span>
|
||||
<span className="absolute left-4 -top-2 text-[9px] font-black text-emerald-500">Y</span>
|
||||
<span className="absolute right-0 bottom-0 text-[9px] font-black text-blue-500">Z</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-0.5 font-mono text-[9px] text-slate-500">
|
||||
<div>X {Math.round(pose.rotateX)}°</div>
|
||||
<div>Y {Math.round(pose.rotateY)}°</div>
|
||||
<div>Z {Math.round(pose.rotateZ)}°</div>
|
||||
</div>
|
||||
</div>
|
||||
<OrientationGizmo pose={pose} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1217,7 +1277,7 @@ export default function ProjectLibrary({ onReverse }: { onReverse: (projId: stri
|
||||
</button>
|
||||
<button
|
||||
onClick={resetModelTransformPose}
|
||||
className="rounded-md bg-white px-2 py-1 text-[10px] font-bold text-slate-600 shadow-sm border border-slate-100 hover:bg-slate-100"
|
||||
className="rounded-md bg-white px-2 py-1 text-[10px] font-bold text-blue-600 shadow-sm border border-slate-100 hover:bg-blue-50"
|
||||
>
|
||||
重置平移缩放位姿
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user