diff --git a/apps/aorta/package.json b/apps/aorta/package.json index 6884092..123e8da 100644 --- a/apps/aorta/package.json +++ b/apps/aorta/package.json @@ -20,7 +20,8 @@ "mitt": "3.0.1", "@tavi/i18n": "^1.5.0", "@tavi/util": "1.0.0", - "js-cookie": "3.0.5" + "js-cookie": "3.0.5", + "three": "0.156.1" }, "devDependencies": { "@babel/core": "^7.21.8", @@ -62,6 +63,7 @@ "webpack-dev-server": "^4.13.3", "webpack-merge": "^5.8.0", "webpackbar": "^5.0.2", - "@types/js-cookie": "3.0.3" + "@types/js-cookie": "3.0.3", + "@types/three": "0.155.1" } } \ No newline at end of file diff --git a/apps/aorta/public/1.stl b/apps/aorta/public/1.stl new file mode 100644 index 0000000..7edb2ca Binary files /dev/null and b/apps/aorta/public/1.stl differ diff --git a/apps/aorta/src/modules/Root/Viewer/Root/STLViewer.tsx b/apps/aorta/src/modules/Root/Viewer/Root/STLViewer.tsx new file mode 100644 index 0000000..a759436 --- /dev/null +++ b/apps/aorta/src/modules/Root/Viewer/Root/STLViewer.tsx @@ -0,0 +1,156 @@ +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"; + +export const STLViewer: React.FC = () => { + const containerRef = useRef(null); + + useEffect(() => { + if (containerRef.current) { + const width = containerRef.current.clientWidth; + const height = containerRef.current.clientHeight; + + // 创建场景、相机和渲染器 + const scene = new THREE.Scene(); + const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); + camera.position.z = 5; + + const renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(width, height); + containerRef.current.appendChild(renderer.domElement); + + // 加载STL文件 + const loader = new STLLoader(); + loader.load("/1.stl", (geometry) => { + const s = autoScale(geometry); + // geometry.center(); + // const material = new THREE.MeshPhongMaterial({ + // color: "lightgrey", // 使用更亮的颜色 + // shininess: 100, // 增加亮度 + // specular: 0x222222, // 调整镜面高光的颜色 + // // side: THREE.DoubleSide, + // // wireframe: true, + // }); + // const mesh = new THREE.Mesh(geometry, material); + // mesh.scale.set(scale, scale, scale); + // scene.add(mesh); + + // 输出三角形顶点信息 + const triangles = getTriangleVertexs(geometry); + //////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////// + + const newTriangles = []; + + triangles.forEach((triangleVertices) => { + const A = triangleVertices[0]; + const B = triangleVertices[1]; + const C = triangleVertices[2]; + + // 计算三角形的法线 + 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) + ); + g.setIndex(indices); + + // 为了确保光照和阴影正确,我们需要计算几何体的面的法线 + g.computeVertexNormals(); + g.center(); + + // 创建网格使用MeshPhongMaterial或其他的材料 + const material = new THREE.MeshPhongMaterial({ + color: 0x00ff00, + side: THREE.DoubleSide, + }); + + const mesh = new THREE.Mesh(g, material); + mesh.scale.set(s, s, s); + + // 添加网格到场景 + scene.add(mesh); + + //////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////// + }); + + // 添加环境光源 + const ambientLight = new THREE.AmbientLight(0xffffff, 1); // 第二个参数为强度 + scene.add(ambientLight); + + // 添加方向光源 + const directionalLight = new THREE.DirectionalLight(0xffffff, 2); + directionalLight.position.set(1, 1, 1).normalize(); + scene.add(directionalLight); + + // 添加XYZ轴 + const axesHelper = new THREE.AxesHelper(2); + scene.add(axesHelper); + + // 添加OrbitControls + const controls = new OrbitControls(camera, renderer.domElement); + + // 渲染函数 + const animate = () => { + requestAnimationFrame(animate); + renderer.render(scene, camera); + }; + + animate(); + } + }, []); + + return
; +}; diff --git a/apps/aorta/src/modules/Root/Viewer/Root/index.tsx b/apps/aorta/src/modules/Root/Viewer/Root/index.tsx index a134be0..5d2ecad 100644 --- a/apps/aorta/src/modules/Root/Viewer/Root/index.tsx +++ b/apps/aorta/src/modules/Root/Viewer/Root/index.tsx @@ -1,7 +1,13 @@ +import { STLViewer } from "./STLViewer"; + interface RootViewerProps { children?: JSX.Element; } export const RootViewer = (props: RootViewerProps) => { - return
RootViewer
; + return ( +
+ +
+ ); }; diff --git a/apps/aorta/src/modules/Root/Viewer/Root/util.ts b/apps/aorta/src/modules/Root/Viewer/Root/util.ts new file mode 100644 index 0000000..3b2713c --- /dev/null +++ b/apps/aorta/src/modules/Root/Viewer/Root/util.ts @@ -0,0 +1,68 @@ +import * as THREE from "three"; + +export const autoScale = (geometry: THREE.BufferGeometry) => { + // 计算模型的边界 + geometry.computeBoundingBox(); + // 获取模型的大小 + const modelSize = geometry.boundingBox!.getSize(new THREE.Vector3()); + const maxSize = Math.max(modelSize.x, modelSize.y, modelSize.z); + // 根据模型的大小设置一个缩放因子 + const scaleFactor = 2 / maxSize; // 假设我们希望模型最大为2单位 + return scaleFactor; +}; + +export const getTriangleVertexs = (geometry: THREE.BufferGeometry) => { + console.time("计算三角面面片顶点"); + const positions = geometry.attributes.position; + const triangles = []; + + 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); + } + } 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); + } + } + console.timeEnd("计算三角面面片顶点"); + return triangles; +}; diff --git a/apps/services/dmp/gateway/src/dicom/dicom.controller.ts b/apps/services/dmp/gateway/src/dicom/dicom.controller.ts index 22ffc5f..5975eae 100644 --- a/apps/services/dmp/gateway/src/dicom/dicom.controller.ts +++ b/apps/services/dmp/gateway/src/dicom/dicom.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Inject, Param, Post, Res } from '@nestjs/common'; +import { Body, Controller, Post, Inject, Res } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { Response } from 'express'; import { firstValueFrom } from 'rxjs'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cb4378..f78f2d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ importers: react-router-dom: specifier: 6.14.1 version: 6.14.1(react-dom@18.2.0)(react@18.2.0) + three: + specifier: 0.156.1 + version: registry.npmmirror.com/three@0.156.1 devDependencies: '@babel/core': specifier: ^7.21.8 @@ -86,6 +89,9 @@ importers: '@types/react-router-dom': specifier: 5.3.3 version: 5.3.3 + '@types/three': + specifier: 0.155.1 + version: registry.npmmirror.com/@types/three@0.155.1 babel-loader: specifier: ^9.1.2 version: 9.1.2(@babel/core@7.21.8)(webpack@5.75.0) @@ -12727,6 +12733,12 @@ packages: name: '@tsconfig/node16' version: 1.0.4 + registry.npmmirror.com/@tweenjs/tween.js@18.6.4: + resolution: {integrity: sha512-lB9lMjuqjtuJrx7/kOkqQBtllspPIN+96OvTCeJ2j5FEzinoAXTdAMFnDAQT1KVPRlnYfBrqxtqP66vDM40xxQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-18.6.4.tgz} + name: '@tweenjs/tween.js' + version: 18.6.4 + dev: true + registry.npmmirror.com/@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.2.tgz} name: '@types/body-parser' @@ -12882,6 +12894,31 @@ packages: '@types/node': registry.npmmirror.com/@types/node@20.3.3 dev: true + registry.npmmirror.com/@types/stats.js@0.17.0: + resolution: {integrity: sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.0.tgz} + name: '@types/stats.js' + version: 0.17.0 + dev: true + + registry.npmmirror.com/@types/three@0.155.1: + resolution: {integrity: sha512-uNUwnz/wWRxahjIqTtDYQ1qdE1R1py21obxfuILkT+kKrjocMwRLQQA1l8nMxfQU7VXb7CXu04ucMo8OqZt4ZA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/three/-/three-0.155.1.tgz} + name: '@types/three' + version: 0.155.1 + dependencies: + '@tweenjs/tween.js': registry.npmmirror.com/@tweenjs/tween.js@18.6.4 + '@types/stats.js': registry.npmmirror.com/@types/stats.js@0.17.0 + '@types/webxr': registry.npmmirror.com/@types/webxr@0.5.4 + fflate: registry.npmmirror.com/fflate@0.6.10 + lil-gui: registry.npmmirror.com/lil-gui@0.17.0 + meshoptimizer: registry.npmmirror.com/meshoptimizer@0.18.1 + dev: true + + registry.npmmirror.com/@types/webxr@0.5.4: + resolution: {integrity: sha512-41gfGLTtqXZhcmoDlLDHqMJDuwAMwhHwXf9Q2job3TUBsvkNfPNI/3IWVEtLH4tyY1ElWtfwIaoNeqeEX238/Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.4.tgz} + name: '@types/webxr' + version: 0.5.4 + dev: true + registry.npmmirror.com/@typescript-eslint/eslint-plugin@5.61.0(@typescript-eslint/parser@5.61.0)(eslint@8.44.0)(typescript@5.1.3): resolution: {integrity: sha512-A5l/eUAug103qtkwccSCxn8ZRwT+7RXWkFECdA4Cvl1dOlDUgTpAOfSEElZn2uSUxhdDpnCdetrf0jvU4qrL+g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz} id: registry.npmmirror.com/@typescript-eslint/eslint-plugin/5.61.0 @@ -14866,6 +14903,12 @@ packages: reusify: 1.0.4 dev: true + registry.npmmirror.com/fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fflate/-/fflate-0.6.10.tgz} + name: fflate + version: 0.6.10 + dev: true + registry.npmmirror.com/figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/figures/-/figures-3.2.0.tgz} name: figures @@ -15693,6 +15736,12 @@ packages: type-check: registry.npmmirror.com/type-check@0.4.0 dev: true + registry.npmmirror.com/lil-gui@0.17.0: + resolution: {integrity: sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lil-gui/-/lil-gui-0.17.0.tgz} + name: lil-gui + version: 0.17.0 + dev: true + registry.npmmirror.com/loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz} name: loader-runner @@ -15877,6 +15926,12 @@ packages: engines: {node: '>= 8'} dev: true + registry.npmmirror.com/meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz} + name: meshoptimizer + version: 0.18.1 + dev: true + registry.npmmirror.com/methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz} name: methods @@ -17358,6 +17413,12 @@ packages: any-promise: registry.npmmirror.com/any-promise@1.3.0 dev: false + registry.npmmirror.com/three@0.156.1: + resolution: {integrity: sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/three/-/three-0.156.1.tgz} + name: three + version: 0.156.1 + dev: false + registry.npmmirror.com/through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/through/-/through-2.3.8.tgz} name: through