From 8fb43bd4792e0868f34afe6105ddf1b3db54d530 Mon Sep 17 00:00:00 2001 From: mozzie Date: Wed, 6 Sep 2023 17:01:45 +0800 Subject: [PATCH] feat: stl wall thickness --- DockerFile | 60 +++++ PyBlender.dockerfile | 24 ++ add_thickness.py | 43 ++++ api_script.py | 19 ++ .../modules/Root/Viewer/Root/STLViewer.tsx | 82 ++----- .../src/modules/Root/Viewer/Root/util.ts | 220 ++++++++++++++---- 6 files changed, 339 insertions(+), 109 deletions(-) create mode 100644 DockerFile create mode 100644 PyBlender.dockerfile create mode 100644 add_thickness.py create mode 100644 api_script.py diff --git a/DockerFile b/DockerFile new file mode 100644 index 0000000..7dc7662 --- /dev/null +++ b/DockerFile @@ -0,0 +1,60 @@ +FROM ubuntu:latest + +# 使用阿里云的Ubuntu源替换默认源 +# RUN echo "deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse" > /etc/apt/sources.list && \ +# echo "deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse" >> /etc/apt/sources.list && \ +# echo "deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse" >> /etc/apt/sources.list && \ +# echo "deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse" >> /etc/apt/sources.list && \ +# echo "deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse" >> /etc/apt/sources.list + +# 更新软件包列表并安装依赖项 +RUN apt-get update && apt-get install -y \ + software-properties-common \ + libgl1-mesa-glx \ + libxi6 \ + libxxf86vm1 \ + libxfixes3 \ + libxrender1 \ + libx11-6 \ + libsdl2-2.0-0 \ + libopenal1 \ + libxrandr2 \ + libfreetype6 \ + libtiff5 \ + libjpeg8 \ + libpng16-16 \ + zlib1g \ + wget \ + python3 \ + python3-pip \ + libxi6 \ + libglu1-mesa \ + libxext6 + +# 下载并安装Blender +RUN wget https://mirrors.tuna.tsinghua.edu.cn/blender/release/Blender2.93/blender-2.93.1-linux-x64.tar.xz && \ + tar xf blender-2.93.1-linux-x64.tar.xz && \ + mv blender-2.93.1-linux-x64 /opt/blender && \ + rm blender-2.93.1-linux-x64.tar.xz + +# 将Blender的可执行文件路径添加到PATH中 +ENV PATH="$PATH:/opt/blender" + +# 安装Python依赖项 +RUN pip3 install fastapi[all] uvicorn + +# 工作目录 +WORKDIR /workspace + +RUN mkdir /workspace/temp + + +# 将你的API代码和Blender Python脚本复制到工作目录中 +COPY ./api_script.py /workspace/ +COPY ./add_thickness.py /workspace/ + +# 暴露API使用的端口 +EXPOSE 8000 + +# 启动API +CMD ["uvicorn", "api_script:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/PyBlender.dockerfile b/PyBlender.dockerfile new file mode 100644 index 0000000..86f58c1 --- /dev/null +++ b/PyBlender.dockerfile @@ -0,0 +1,24 @@ +# 使用预先构建的Blender镜像 +FROM zocker160/blender-bpy:stable + +# 安装Python库和工具 +RUN apt-get update && apt-get install -y \ + python3-pip + +# 安装Python依赖 +RUN pip3 install fastapi uvicorn python-multipart + +# 设置工作目录 +WORKDIR /workspace + +RUN mkdir /workspace/temp + +# 复制API和处理脚本到容器内 +COPY ./api_script.py /workspace/ +COPY ./add_thickness.py /workspace/ + +# 暴露FastAPI使用的端口 +EXPOSE 8000 + +# 设置启动命令 +CMD ["uvicorn", "api_script:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/add_thickness.py b/add_thickness.py new file mode 100644 index 0000000..73be743 --- /dev/null +++ b/add_thickness.py @@ -0,0 +1,43 @@ +import bpy +import os + +def cleanup(): + # 删除所有meshes,这样我们可以从一个干净的环境开始 + bpy.ops.object.select_all(action='DESELECT') + bpy.ops.object.select_by_type(type='MESH') + bpy.ops.object.delete() + +def add_thickness_to_stl(input_file_path, output_file_path, thickness=0.1): + cleanup() + + # 激活STL导入插件 + bpy.ops.preferences.addon_enable(module="io_mesh_stl") + + # 导入STL + bpy.ops.import_mesh.stl(filepath=input_file_path) + + # 确保导入的模型是当前活跃的 + obj = bpy.context.selected_objects[0] + bpy.context.view_layer.objects.active = obj + + # 进入编辑模式 + bpy.ops.object.editmode_toggle() + + # 选择所有顶点 + bpy.ops.mesh.select_all(action='SELECT') + + # 使用Solidify修饰器来增加壁厚 + bpy.ops.object.modifier_add(type='SOLIDIFY') + solidify = obj.modifiers["Solidify"] + solidify.thickness = thickness + + # 应用修饰器 + bpy.ops.object.modifier_apply({"object": obj}, modifier="Solidify") + + # 返回对象模式 + bpy.ops.object.editmode_toggle() + + # 导出为STL + bpy.ops.export_mesh.stl(filepath=output_file_path) + + cleanup() diff --git a/api_script.py b/api_script.py new file mode 100644 index 0000000..1b96104 --- /dev/null +++ b/api_script.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, UploadFile, File +from add_thickness import add_thickness_to_stl + +app = FastAPI() + +@app.post("/process_stl/") +async def process_stl(file: UploadFile = File(...)): + input_file_path = f"temp/{file.filename}" + output_file_path = f"processed/{file.filename}" + + # 保存上传的文件 + with open(input_file_path, 'wb') as buffer: + buffer.write(file.file.read()) + + # 调用修改过的 add_thickness 函数 + add_thickness_to_stl(input_file_path, output_file_path) + + # 返回处理后的文件 + return FileResponse(output_file_path, headers={"Content-Disposition": f"attachment; filename={file.filename}"}) diff --git a/apps/aorta/src/modules/Root/Viewer/Root/STLViewer.tsx b/apps/aorta/src/modules/Root/Viewer/Root/STLViewer.tsx index a759436..1c7608b 100644 --- a/apps/aorta/src/modules/Root/Viewer/Root/STLViewer.tsx +++ b/apps/aorta/src/modules/Root/Viewer/Root/STLViewer.tsx @@ -2,7 +2,12 @@ import React, { useEffect, useRef } from "react"; import * as THREE from "three"; import { STLLoader } from "three/examples/jsm/loaders/STLLoader"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; -import { autoScale, getTriangleVertexs } from "./util"; +import { + autoScale, + createEnclosedGeometry, + getOffsetTriangles, + getTriangleVertexs, +} from "./util"; export const STLViewer: React.FC = () => { const containerRef = useRef(null); @@ -44,80 +49,25 @@ export const STLViewer: React.FC = () => { //////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////// - const newTriangles = []; + const newTriangles = getOffsetTriangles(triangles, -2); - triangles.forEach((triangleVertices) => { - const A = triangleVertices[0]; - const B = triangleVertices[1]; - const C = triangleVertices[2]; + console.log("newTriangles", newTriangles); - // 计算三角形的法线 - let normal = new THREE.Vector3(); - let cb = new THREE.Vector3(); - let ab = new THREE.Vector3(); - cb.subVectors(C, B); - ab.subVectors(A, B); - cb.cross(ab).normalize(); - normal.copy(cb); - - // 创建新的顶点,它们沿着法线方向内移2个单位 - let A1 = new THREE.Vector3() - .copy(A) - .add(normal.clone().multiplyScalar(-2)); - let B1 = new THREE.Vector3() - .copy(B) - .add(normal.clone().multiplyScalar(-2)); - let C1 = new THREE.Vector3() - .copy(C) - .add(normal.clone().multiplyScalar(-2)); - - // 添加新的三角形 - newTriangles.push([A1, B1, C1]); - - // 创建封闭表面的新面片 - newTriangles.push([A, B, A1]); - newTriangles.push([B, B1, A1]); - - newTriangles.push([B, C, B1]); - newTriangles.push([C, C1, B1]); - - newTriangles.push([C, A, C1]); - newTriangles.push([A, A1, C1]); - }); - - const g = new THREE.BufferGeometry(); - - // 将newTriangles数据分解为顶点和索引数组 - const vertices = []; - const indices = []; - newTriangles.forEach((triangle, index) => { - triangle.forEach((vertex) => { - vertices.push(vertex.x, vertex.y, vertex.z); - }); - indices.push(index * 3, index * 3 + 1, index * 3 + 2); - }); - - // 设置vertices和indices到geometry - g.setAttribute( - "position", - new THREE.Float32BufferAttribute(vertices, 3) + // 使用上面定义的函数 + const enclosedGeometry = createEnclosedGeometry( + triangles, + newTriangles ); - g.setIndex(indices); - // 为了确保光照和阴影正确,我们需要计算几何体的面的法线 - g.computeVertexNormals(); - g.center(); + enclosedGeometry.center(); - // 创建网格使用MeshPhongMaterial或其他的材料 - const material = new THREE.MeshPhongMaterial({ - color: 0x00ff00, + const material = new THREE.MeshBasicMaterial({ + color: "lightgrey", side: THREE.DoubleSide, }); - const mesh = new THREE.Mesh(g, material); + const mesh = new THREE.Mesh(enclosedGeometry, material); mesh.scale.set(s, s, s); - - // 添加网格到场景 scene.add(mesh); //////////////////////////////////////////////////////////////// diff --git a/apps/aorta/src/modules/Root/Viewer/Root/util.ts b/apps/aorta/src/modules/Root/Viewer/Root/util.ts index 3b2713c..13ab224 100644 --- a/apps/aorta/src/modules/Root/Viewer/Root/util.ts +++ b/apps/aorta/src/modules/Root/Viewer/Root/util.ts @@ -11,58 +11,192 @@ export const autoScale = (geometry: THREE.BufferGeometry) => { return scaleFactor; }; -export const getTriangleVertexs = (geometry: THREE.BufferGeometry) => { - console.time("计算三角面面片顶点"); +export const getTriangleVertexs = ( + geometry: THREE.BufferGeometry +): THREE.Vector3[][] => { const positions = geometry.attributes.position; - const triangles = []; + const triangles: THREE.Vector3[][] = []; + + const getTriangleVertices = ( + index1: number, + index2: number, + index3: number + ) => [ + new THREE.Vector3( + positions.getX(index1), + positions.getY(index1), + positions.getZ(index1) + ), + new THREE.Vector3( + positions.getX(index2), + positions.getY(index2), + positions.getZ(index2) + ), + new THREE.Vector3( + positions.getX(index3), + positions.getY(index3), + positions.getZ(index3) + ), + ]; if (geometry.index) { const indices = geometry.index.array; - for (let i = 0; i < indices.length; i += 3) { - const triangleVertices = [ - new THREE.Vector3( - positions.getX(indices[i]), - positions.getY(indices[i]), - positions.getZ(indices[i]) - ), - new THREE.Vector3( - positions.getX(indices[i + 1]), - positions.getY(indices[i + 1]), - positions.getZ(indices[i + 1]) - ), - new THREE.Vector3( - positions.getX(indices[i + 2]), - positions.getY(indices[i + 2]), - positions.getZ(indices[i + 2]) - ), - ]; - - triangles.push(triangleVertices); + triangles.push( + getTriangleVertices(indices[i], indices[i + 1], indices[i + 2]) + ); } } else { for (let i = 0; i < positions.count; i += 3) { - const triangleVertices = [ - new THREE.Vector3( - positions.getX(i), - positions.getY(i), - positions.getZ(i) - ), - new THREE.Vector3( - positions.getX(i + 1), - positions.getY(i + 1), - positions.getZ(i + 1) - ), - new THREE.Vector3( - positions.getX(i + 2), - positions.getY(i + 2), - positions.getZ(i + 2) - ), - ]; - - triangles.push(triangleVertices); + triangles.push(getTriangleVertices(i, i + 1, i + 2)); } } - console.timeEnd("计算三角面面片顶点"); + return triangles; }; + +export const getOffsetTriangles = ( + triangles: THREE.Vector3[][], + offset: number +): THREE.Vector3[][] => { + const newTriangles: THREE.Vector3[][] = []; + + triangles.forEach((triangle) => { + // 计算三角形的法线 + const normal = new THREE.Vector3(); + const edge1 = new THREE.Vector3().subVectors(triangle[1], triangle[0]); + const edge2 = new THREE.Vector3().subVectors(triangle[2], triangle[0]); + normal.crossVectors(edge1, edge2).normalize(); + + // 使用法线和偏移量计算新的三角形顶点 + const offsetNormal = normal.clone().multiplyScalar(offset); + const newTriangle = triangle.map((vertex) => + vertex.clone().sub(offsetNormal) + ); + newTriangles.push(newTriangle); + }); + + return newTriangles; +}; + +export const createEnclosedGeometry = ( + triangles: THREE.Vector3[][], + newTriangles: THREE.Vector3[][] +): THREE.BufferGeometry => { + const geometry = new THREE.BufferGeometry(); + const vertices: THREE.Vector3[] = []; + const indices: number[] = []; + + const processedEdges: Set = new Set(); + + function edgeId(v1: THREE.Vector3, v2: THREE.Vector3): string { + return `${v1.x},${v1.y},${v1.z}->${v2.x},${v2.y},${v2.z}`; + } + + triangles.forEach((triangle, index) => { + const innerTriangle = newTriangles[index]; + + triangle.forEach((vertex) => { + vertices.push(vertex); + }); + + innerTriangle.forEach((vertex) => { + vertices.push(vertex); + }); + + // Process the three edges for each triangle + for (let i = 0; i < 3; i++) { + const next = (i + 1) % 3; + const edge = edgeId(triangle[i], triangle[next]); + + if (!processedEdges.has(edge)) { + processedEdges.add(edge); + + // Add the vertices for the quad (two triangles) + const quadVertices = [ + triangle[i], + triangle[next], + innerTriangle[i], + innerTriangle[next], + ]; + + quadVertices.forEach((vertex) => { + vertices.push(vertex); + }); + + const baseIndex = vertices.length - 4; + + // First triangle of the quad + indices.push(baseIndex, baseIndex + 2, baseIndex + 1); + // Second triangle of the quad + indices.push(baseIndex + 1, baseIndex + 2, baseIndex + 3); + } + } + }); + + geometry.setFromPoints(vertices); + geometry.setIndex(indices); + + return geometry; +}; + +// export const createEnclosedGeometry = ( +// triangles: THREE.Vector3[][], +// newTriangles: THREE.Vector3[][] +// ): THREE.BufferGeometry => { +// const geometry = new THREE.BufferGeometry(); + +// const vertices: number[] = []; +// const indices: number[] = []; + +// // 添加原始三角形和新三角形的顶点到vertices数组 +// triangles.forEach((triangle) => { +// triangle.forEach((v) => { +// vertices.push(v.x, v.y, v.z); +// }); +// }); + +// newTriangles.forEach((triangle) => { +// triangle.forEach((v) => { +// vertices.push(v.x, v.y, v.z); +// }); +// }); + +// const totalTriangles = triangles.length; + +// // 创建索引来连接triangles和newTriangles中的三角形 +// for (let i = 0; i < totalTriangles; i++) { +// const offset = totalTriangles * 3; // newTriangles在vertices中的偏移量 + +// const a1 = i * 3; +// const b1 = a1 + 1; +// const c1 = a1 + 2; + +// const a2 = offset + i * 3; +// const b2 = a2 + 1; +// const c2 = a2 + 2; + +// // 添加原始和新三角形的索引 +// indices.push(a1, b1, c1); +// indices.push(a2, c2, b2); + +// // 为每一对相邻的三角形添加边的索引 +// indices.push(a1, b1, a2); +// indices.push(b1, b2, a2); + +// indices.push(b1, c1, b2); +// indices.push(c1, c2, b2); + +// indices.push(c1, a1, c2); +// indices.push(a1, a2, c2); +// } + +// geometry.setIndex(indices); +// geometry.setAttribute( +// "position", +// new THREE.Float32BufferAttribute(vertices, 3) +// ); +// geometry.computeVertexNormals(); // 计算顶点法线 + +// return geometry; +// };