├── .gitignore ├── README.md ├── package.json ├── result.png ├── src ├── assets │ └── geo.json ├── index.css ├── index.html ├── index.ts └── scripts │ ├── app.ts │ ├── clip.ts │ └── lut.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three.js-clipping 2 | 3 | 用 three.js 实现的三维地质模型剖切,以及剖面的补充 4 | 5 | > 当模型由多个子模型构成时,效率上有问题,还需要后续处理 6 | 7 | ## 效果图: 8 | 9 | ![三维地质模型剖切](./result.png) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three.js-clipping", 3 | "version": "1.0.0", 4 | "description": "用 three.js 实现的三维地质模型剖切,以及剖面的补充", 5 | "author": "screamsyk", 6 | "license": "ISC", 7 | "scripts": { 8 | "start": "webpack-dev-server --progress --open --inline", 9 | "build": "rimraf ./dist && webpack --mode=development" 10 | }, 11 | "devDependencies": { 12 | "copy-webpack-plugin": "^5.1.1", 13 | "css-loader": "^3.5.2", 14 | "html-webpack-plugin": "^4.2.0", 15 | "style-loader": "^1.1.4", 16 | "ts-loader": "^7.0.0", 17 | "typescript": "^3.8.3", 18 | "webpack": "^4.42.1", 19 | "webpack-cli": "^3.3.11", 20 | "webpack-dev-server": "^3.10.3" 21 | }, 22 | "dependencies": { 23 | "axios": "^0.19.2", 24 | "three": "^0.115.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wincy94/three.js-clipping/9bd6a989fa9b7e3d73929ed486e72427e24c521c/result.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html, 8 | body { 9 | height: 100%; 10 | overflow: hidden; 11 | } 12 | 13 | canvas { 14 | height: 100%; 15 | width: 100%; 16 | } 17 | 18 | .content { 19 | position: relative; 20 | height: 100%; 21 | } 22 | 23 | .control { 24 | position: absolute; 25 | bottom: 20px; 26 | left: 50%; 27 | width: 300px; 28 | height: 50px; 29 | line-height: 50px; 30 | margin-left: -150px; 31 | background-color: rgba(0, 0, 0, .45); 32 | color: #fff; 33 | border-radius: 4px; 34 | text-align: center; 35 | font-size: 17px; 36 | font-weight: bold; 37 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 模型剖切 8 | 9 | 10 | 11 |
12 | 13 |
三维地质模型剖切
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import { App } from './scripts/app'; 3 | new App(); -------------------------------------------------------------------------------- /src/scripts/app.ts: -------------------------------------------------------------------------------- 1 | import { Scene, PerspectiveCamera, WebGLRenderer, AxesHelper, AmbientLight, DirectionalLight, Vector3, MOUSE, Group, BufferGeometry, BufferAttribute, MeshLambertMaterial, DoubleSide, Mesh, EdgesGeometry, LineSegments, LineBasicMaterial, Object3D } from 'three'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 3 | import axios from 'axios'; 4 | import { Lut } from './lut'; 5 | import { Clip } from './clip'; 6 | 7 | export class App { 8 | 9 | // (1)基本数据 10 | private scene: Scene; 11 | private camera: PerspectiveCamera; 12 | private renderer: WebGLRenderer; 13 | private controls: OrbitControls; 14 | private clip: Clip | null = null; // 剖切 15 | 16 | /**(2)构造函数 */ 17 | constructor() { 18 | const canvas = document.getElementById('canvasId') as HTMLCanvasElement; 19 | this.scene = new Scene(); 20 | this.camera = new PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 100000); 21 | this.renderer = new WebGLRenderer({ antialias: true, canvas: canvas }); 22 | this.controls = new OrbitControls(this.camera, this.renderer.domElement); 23 | this.initScene(); 24 | this.initCamera(); 25 | this.initRenderer(); 26 | this.initControls(); 27 | this.render(); 28 | } 29 | 30 | /**(3)初始化场景 */ 31 | initScene() { 32 | this.scene.add(new AxesHelper(100000)); 33 | this.scene.add(new AmbientLight(0xffffff, 0.5)); 34 | const directionLight = new DirectionalLight(0xffffff, 1); 35 | directionLight.position.set(200, 200, 200); 36 | this.scene.add(directionLight); 37 | this.load().then((obj: any) => { 38 | this.clip = new Clip(obj, this.scene, this.camera, this.renderer, this.controls); 39 | this.clip.open(); 40 | }); 41 | } 42 | 43 | /**(4)初始化照相机 */ 44 | initCamera() { 45 | this.camera.position.set(0, 1500, 1000); 46 | this.camera.lookAt(new Vector3(0, 0, 0)); 47 | } 48 | 49 | /**(5)初始化渲染器 */ 50 | initRenderer() { 51 | this.renderer.localClippingEnabled = true; 52 | this.renderer.autoClear = false; 53 | this.renderer.setClearColor(0xdedede); 54 | this.renderer.setPixelRatio(window.devicePixelRatio); 55 | this.renderer.setSize(window.innerWidth, window.innerHeight); 56 | window.addEventListener('resize', this.onWindowResize); 57 | } 58 | 59 | /**(6)初始化控制器 */ 60 | initControls() { 61 | this.controls.mouseButtons = { 62 | LEFT: MOUSE.PAN, 63 | MIDDLE: MOUSE.DOLLY, 64 | RIGHT: MOUSE.ROTATE 65 | } 66 | } 67 | 68 | /**(7)渲染 */ 69 | private render = () => { 70 | this.renderer.clear(); 71 | if (this.clip?.isOpen) { 72 | this.clip.stencilTest(); 73 | } 74 | this.renderer.render(this.scene, this.camera); 75 | requestAnimationFrame(this.render); 76 | } 77 | 78 | /**(8)窗口大小改变时,更新画布大小 */ 79 | private onWindowResize = () => { 80 | this.camera.aspect = window.innerWidth / window.innerHeight; 81 | this.camera.updateProjectionMatrix(); 82 | this.renderer.setSize(window.innerWidth, window.innerHeight); 83 | } 84 | 85 | 86 | // ---------------------模型相关-------------------------- 87 | 88 | //(1)基本数据 89 | 90 | /**(2)加载模型 */ 91 | load() { 92 | return new Promise((resolve) => { 93 | axios.request({ url: 'assets/geo.json' }).then((res) => { 94 | const group = new Group(); 95 | const lutColor = new Lut('rainbow', 512); 96 | lutColor.setMin(0); 97 | lutColor.setMax(4); 98 | const gridList = res.data.GridList; 99 | gridList.forEach((item: any) => { 100 | const geometry = new BufferGeometry(); 101 | 102 | // 指定立方体的八个顶点的 xyz 103 | const vertices = new Float32Array(24); 104 | let i = 0; 105 | item.GridPoints.forEach(function (point: any) { 106 | vertices[i * 3] = point.x; 107 | vertices[(i * 3) + 1] = point.y; 108 | vertices[(i * 3) + 2] = point.z; 109 | i++; 110 | }); 111 | geometry.setAttribute('position', new BufferAttribute(vertices, 3)); 112 | 113 | // 复用三角片的顶点 114 | const indexes = new Uint16Array([ 115 | 0, 1, 3, 116 | 2, 1, 3, 117 | 1, 0, 6, 118 | 5, 6, 0, 119 | 0, 3, 5, 120 | 4, 5, 3, 121 | 7, 4, 6, 122 | 5, 4, 6, 123 | 1, 2, 6, 124 | 7, 6, 2, 125 | 2, 3, 7, 126 | 4, 3, 7 127 | ]); 128 | geometry.index = new BufferAttribute(indexes, 1); 129 | 130 | // 设定颜色 131 | const ZValue = item.GridPropertys[0].PropertyValue; 132 | const facecolor = lutColor.getColor(ZValue); 133 | const material = new MeshLambertMaterial({ 134 | color: facecolor, 135 | side: DoubleSide 136 | }); 137 | 138 | // 添加该立方体 139 | const mesh = new Mesh(geometry, material); 140 | const edges = new EdgesGeometry(geometry, 10); 141 | const line = new LineSegments(edges, new LineBasicMaterial({ color: 0xffffff, opacity: 0.3 })); 142 | group.add(line); 143 | group.add(mesh); 144 | }); 145 | this.scene.add(group); 146 | resolve(group); 147 | }); 148 | }); 149 | } 150 | 151 | } -------------------------------------------------------------------------------- /src/scripts/clip.ts: -------------------------------------------------------------------------------- 1 | import { Object3D, Scene, WebGLRenderer, Box3, Geometry, Vector3, Mesh, Face3, MeshBasicMaterial, Vector2, Raycaster, PerspectiveCamera, Group, LineSegments, LineBasicMaterial, Plane, PlaneGeometry, BackSide, FrontSide, BoxGeometry, ShaderMaterial } from 'three'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 3 | 4 | /**类:模型剖切 */ 5 | export class Clip { 6 | 7 | // (1)基本数据 8 | private obj: Object3D; 9 | private scene: Scene; 10 | private camera: PerspectiveCamera; 11 | private renderer: WebGLRenderer; 12 | private controls: OrbitControls; 13 | isOpen: boolean = false; 14 | 15 | /** 16 | * (2)构造函数 17 | * @param obj 剖切的模型对象 18 | * @param scene 场景 19 | * @param camera 照相机 20 | * @param renderer 渲染器 21 | * @param controls 控制器 22 | */ 23 | constructor(obj: Object3D, scene: Scene, camera: PerspectiveCamera, renderer: WebGLRenderer, controls: OrbitControls) { 24 | this.obj = obj; 25 | this.scene = scene; 26 | this.camera = camera; 27 | this.renderer = renderer; 28 | this.controls = controls; 29 | } 30 | 31 | /**(3)开启剖切 */ 32 | open() { 33 | this.initClipBox(); 34 | // this.initCapBoxList(); 35 | this.addMouseListener(); 36 | // this.isOpen = true; 37 | } 38 | 39 | /**(4)关闭剖切 */ 40 | close() { 41 | this.isOpen = false; 42 | this.removeMouseListener(); 43 | this.clearClipBox(); 44 | } 45 | 46 | /**(5)重置剖切 */ 47 | reset() { 48 | this.close(); 49 | this.open(); 50 | } 51 | 52 | 53 | // ---------------剖切盒相关-------------------- 54 | 55 | // (1)基本数据 56 | private low: Vector3 = new Vector3(); // 最低点 57 | private high: Vector3 = new Vector3(); // 最高点 58 | private low_init: Vector3 = new Vector3(); 59 | private high_init: Vector3 = new Vector3(); 60 | private group: Group = new Group();; // 记录开启剖切后添加的所有对象 61 | private planes: Array = []; // 剖切平面 62 | private vertices = [ 63 | new Vector3(), new Vector3(), new Vector3(), new Vector3(), // 顶部 4 个 64 | new Vector3(), new Vector3(), new Vector3(), new Vector3() // 底部 4 个 65 | ] 66 | private faces: Array = []; 67 | private lines: Array = []; 68 | 69 | /**(2)初始化剖切盒 */ 70 | private initClipBox() { 71 | const box3 = new Box3(); 72 | box3.setFromObject(this.obj); // 获取模型对象的边界 73 | this.low = box3.min; 74 | this.high = box3.max; 75 | this.low_init.copy(this.low); // 保留一下初始值,好作为限制条件 76 | this.high_init.copy(this.high); 77 | this.group = new Group(); 78 | this.initPlanes(); 79 | this.initVertices(); 80 | this.initFaces(); 81 | this.initLines(); 82 | this.scene.add(this.group); 83 | } 84 | 85 | /**(3)初始化剖切盒的 6 个剖切平面 */ 86 | private initPlanes() { 87 | this.planes = []; 88 | this.planes.push( 89 | new Plane(new Vector3(0, -1, 0), this.high.y), // 上 90 | new Plane(new Vector3(0, 1, 0), -this.low.y), // 下 91 | new Plane(new Vector3(1, 0, 0), -this.low.x), // 左 92 | new Plane(new Vector3(-1, 0, 0), this.high.x), // 右 93 | new Plane(new Vector3(0, 0, -1), this.high.z), // 前 94 | new Plane(new Vector3(0, 0, 1), -this.low.z), // 后 95 | ); 96 | this.obj.traverse((child: any) => { 97 | if (['Mesh', 'LineSegments'].includes(child.type)) { 98 | child.material.clippingPlanes = this.planes; 99 | } 100 | }); 101 | } 102 | 103 | /**(4)初始化剖切盒的 8 个顶点 */ 104 | private initVertices() { 105 | this.vertices[0].set(this.low.x, this.high.y, this.low.z); 106 | this.vertices[1].set(this.high.x, this.high.y, this.low.z); 107 | this.vertices[2].set(this.high.x, this.high.y, this.high.z); 108 | this.vertices[3].set(this.low.x, this.high.y, this.high.z); 109 | this.vertices[4].set(this.low.x, this.low.y, this.low.z); 110 | this.vertices[5].set(this.high.x, this.low.y, this.low.z); 111 | this.vertices[6].set(this.high.x, this.low.y, this.high.z); 112 | this.vertices[7].set(this.low.x, this.low.y, this.high.z); 113 | } 114 | 115 | /**(5)初始化剖切盒的 6 个面 */ 116 | private initFaces() { 117 | const v = this.vertices; 118 | this.faces = []; 119 | this.faces.push( 120 | new BoxFace('y2', [v[0], v[1], v[2], v[3]]), // 上 y2 121 | new BoxFace('y1', [v[4], v[7], v[6], v[5]]), // 下 y1 122 | new BoxFace('x1', [v[0], v[3], v[7], v[4]]), // 左 x1 123 | new BoxFace('x2', [v[1], v[5], v[6], v[2]]), // 右 x2 124 | new BoxFace('z2', [v[2], v[6], v[7], v[3]]), // 前 z2 125 | new BoxFace('z1', [v[0], v[4], v[5], v[1]]), // 后 z1 126 | ) 127 | this.group.add(...this.faces); 128 | this.faces.forEach(face => { 129 | this.group.add(face.backFace); 130 | }); 131 | } 132 | 133 | /**(6)初始化剖切盒的 12 条边线 */ 134 | private initLines() { 135 | const v = this.vertices; 136 | const f = this.faces; 137 | this.lines = []; 138 | this.lines.push( 139 | new BoxLine([v[0], v[1]], [f[0], f[5]]), 140 | new BoxLine([v[1], v[2]], [f[0], f[3]]), 141 | new BoxLine([v[2], v[3]], [f[0], f[4]]), 142 | new BoxLine([v[3], v[0]], [f[0], f[2]]), 143 | new BoxLine([v[4], v[5]], [f[1], f[5]]), 144 | new BoxLine([v[5], v[6]], [f[1], f[3]]), 145 | new BoxLine([v[6], v[7]], [f[1], f[4]]), 146 | new BoxLine([v[7], v[4]], [f[1], f[2]]), 147 | new BoxLine([v[0], v[4]], [f[2], f[5]]), 148 | new BoxLine([v[1], v[5]], [f[3], f[5]]), 149 | new BoxLine([v[2], v[6]], [f[3], f[4]]), 150 | new BoxLine([v[3], v[7]], [f[2], f[4]]) 151 | ); 152 | this.group.add(...this.lines); 153 | } 154 | 155 | /**(7)清除剖切盒 */ 156 | private clearClipBox() { 157 | this.scene.remove(this.group); 158 | this.obj.traverse((child: any) => { 159 | if (child.type == 'Mesh') { 160 | child.material.clippingPlanes = []; 161 | } 162 | }); 163 | this.renderer.domElement.style.cursor = ''; 164 | } 165 | 166 | 167 | // -------------------覆盖盒(用于填充剖切后的截面,有多个)相关--------------------- 168 | 169 | // (1)基本数据 170 | private capBoxList: Array = []; 171 | private uniforms = { 172 | low: { value: new Vector3() }, 173 | high: { value: new Vector3() } 174 | }; 175 | private vertexShaderSource = ` 176 | varying vec4 worldPosition; 177 | void main() { 178 | worldPosition = modelMatrix * vec4( position, 1.0 ); 179 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 180 | }`; 181 | private framgentShaderSource = ` 182 | uniform vec3 low; 183 | uniform vec3 high; 184 | varying vec4 worldPosition; 185 | void main( void ) { 186 | if((worldPosition.x < low.x && cameraPosition.x < low.x) 187 | || (worldPosition.x > high.x && cameraPosition.x > high.x) 188 | || (worldPosition.y < low.y && cameraPosition.y < low.y) 189 | || (worldPosition.y > high.y && cameraPosition.y > high.y) 190 | || (worldPosition.z < low.z && cameraPosition.z < low.z) 191 | || (worldPosition.z > high.z && cameraPosition.z > high.z)){ 192 | discard; 193 | } else { 194 | gl_FragColor = vec4(0.0,0.0,0.0,1.0); 195 | } 196 | }`; 197 | private frontMaterial = new ShaderMaterial({ 198 | uniforms: this.uniforms, 199 | vertexShader: this.vertexShaderSource, 200 | fragmentShader: this.framgentShaderSource, 201 | colorWrite: false, 202 | depthWrite: false, 203 | side: FrontSide 204 | }) 205 | private backMaterial = new ShaderMaterial({ 206 | uniforms: this.uniforms, 207 | vertexShader: this.vertexShaderSource, 208 | fragmentShader: this.framgentShaderSource, 209 | colorWrite: false, 210 | depthWrite: false, 211 | side: BackSide 212 | }) 213 | 214 | /**(2)初始化覆盖盒 */ 215 | private initCapBoxList() { 216 | this.uniforms.low.value.copy(this.low); 217 | this.uniforms.high.value.copy(this.high); 218 | const list: Array = []; 219 | const len = this.obj.children.length; // 类似的这个固定了,可能要改 220 | for (let i = 0; i < len; i++) { 221 | const mesh: any = this.obj.children[i]; 222 | const item = { 223 | capBox: new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial({ color: mesh.material.color || 0x00ff00 })), 224 | capBoxScene: new Scene(), 225 | backScene: new Scene(), 226 | frontScene: new Scene() 227 | } 228 | item.capBoxScene.add(item.capBox); 229 | 230 | const backObj = this.obj.clone(); 231 | const backMesh = backObj.children[i] as Mesh; 232 | backMesh.material = this.backMaterial; 233 | backObj.children = [backMesh]; 234 | item.backScene.add(backObj); 235 | 236 | const frontObj = this.obj.clone(); 237 | const frontMesh = frontObj.children[i] as Mesh; 238 | frontMesh.material = this.frontMaterial; 239 | frontObj.children = [frontMesh]; 240 | item.frontScene.add(frontObj); 241 | 242 | list.push(item); 243 | } 244 | this.capBoxList = list; 245 | } 246 | 247 | /**(3)更新覆盖盒的大小和中心位置 */ 248 | private updateCapBoxList() { 249 | this.uniforms.low.value.copy(this.low); 250 | this.uniforms.high.value.copy(this.high); 251 | this.capBoxList.forEach(item => { 252 | const size = new Vector3(); 253 | size.subVectors(this.high, this.low); // 大小 254 | const position = this.low.clone().add(size.clone().multiplyScalar(0.5)); // 中心位置 255 | item.capBox.scale.copy(size); 256 | item.capBox.position.copy(position); 257 | }) 258 | } 259 | 260 | /**(4)模板测试,确定覆盖盒要显示的部分 */ 261 | stencilTest() { 262 | this.renderer.clear(); // 清除模板缓存 263 | const gl = this.renderer.getContext(); 264 | gl.enable(gl.STENCIL_TEST); 265 | this.capBoxList.forEach((item, index) => { 266 | 267 | // 初始化模板缓冲值,每层不一样 268 | gl.stencilFunc(gl.ALWAYS, index, 0xff); 269 | gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); 270 | this.renderer.render(item.backScene, this.camera); 271 | 272 | // 背面加1 273 | gl.stencilFunc(gl.ALWAYS, 1, 0xff); 274 | gl.stencilOp(gl.KEEP, gl.KEEP, gl.INCR); 275 | this.renderer.render(item.backScene, this.camera); 276 | 277 | // 正面减1 278 | gl.stencilFunc(gl.ALWAYS, 1, 0xff); 279 | gl.stencilOp(gl.KEEP, gl.KEEP, gl.DECR); 280 | this.renderer.render(item.frontScene, this.camera); 281 | 282 | // 缓冲区为指定值,才显示覆盖盒 283 | gl.stencilFunc(gl.LEQUAL, index + 1, 0xff); 284 | gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); 285 | this.renderer.render(item.capBoxScene, this.camera); 286 | }) 287 | 288 | gl.disable(gl.STENCIL_TEST); 289 | } 290 | 291 | /**(5)判断模型是否与剖切盒相交 */ 292 | private isIntersecting(mesh: any) { 293 | const low = mesh.geometry.boundingBox.min; 294 | const high = mesh.geometry.boundingBox.max; 295 | if ( 296 | low.x >= this.low.x && 297 | low.y >= this.low.y && 298 | low.z >= this.low.z && 299 | high.x <= this.high.x && 300 | high.y <= this.high.y && 301 | high.z <= this.high.z 302 | ) { 303 | return false; 304 | } else { 305 | return true; 306 | } 307 | } 308 | 309 | 310 | // -------------------鼠标操作相关----------------------- 311 | 312 | // (1)基本数据 313 | private raycaster: Raycaster = new Raycaster(); // 光线投射 314 | private mouse: Vector2 = new Vector2(); // 鼠标坐标点 315 | private activeFace: BoxFace | null = null; // 鼠标碰触的面 316 | 317 | /**(2)添加鼠标事件监听 */ 318 | private addMouseListener() { 319 | window.addEventListener('mousemove', this.onMouseMove); 320 | window.addEventListener('mousedown', this.onMouseDown); 321 | } 322 | 323 | /**(3)移除鼠标事件监听 */ 324 | private removeMouseListener() { 325 | window.removeEventListener('mousemove', this.onMouseMove); 326 | window.removeEventListener('mousedown', this.onMouseDown); 327 | } 328 | 329 | /**(4)转换鼠标坐标,并更新射线 */ 330 | private updateMouseAndRay(event: MouseEvent) { 331 | this.mouse.setX((event.clientX / window.innerWidth) * 2 - 1); 332 | this.mouse.setY(-(event.clientY / window.innerHeight) * 2 + 1); 333 | this.raycaster.setFromCamera(this.mouse, this.camera); 334 | } 335 | 336 | /**(5)鼠标移动,处理面的选中状态 */ 337 | private onMouseMove = (event: MouseEvent) => { 338 | this.updateMouseAndRay(event); 339 | const intersects = this.raycaster.intersectObjects(this.faces); // 鼠标与剖切盒的面的相交情况 340 | if (intersects.length) { 341 | this.renderer.domElement.style.cursor = 'pointer'; 342 | const face = intersects[0].object as BoxFace; 343 | if (face !== this.activeFace) { 344 | if (this.activeFace) { 345 | this.activeFace.setActive(false); 346 | } 347 | face.setActive(true); 348 | this.activeFace = face; 349 | } 350 | } else { 351 | if (this.activeFace) { 352 | this.activeFace.setActive(false); 353 | this.activeFace = null; 354 | this.renderer.domElement.style.cursor = 'auto'; 355 | } 356 | } 357 | } 358 | 359 | /**(6)鼠标按下,开始拖动操作 */ 360 | private onMouseDown = (event: MouseEvent) => { 361 | if (this.activeFace) { 362 | this.updateMouseAndRay(event); 363 | const intersects = this.raycaster.intersectObjects(this.faces); // 鼠标与剖切盒的面的相交情况 364 | if (intersects.length) { 365 | const face = intersects[0].object as BoxFace; 366 | const axis = face.axis; 367 | const point = intersects[0].point; 368 | this.drag.start(axis, point); 369 | } 370 | } 371 | } 372 | 373 | /**(7)鼠标拖动,处理剖切操作 */ 374 | private drag = { 375 | axis: '', // 轴线 376 | point: new Vector3(), // 起点 377 | ground: new Mesh(new PlaneGeometry(1000000, 1000000), new MeshBasicMaterial({ colorWrite: false, depthWrite: false })), 378 | start: (axis: string, point: Vector3) => { 379 | this.drag.axis = axis; 380 | this.drag.point = point; 381 | this.drag.initGround(); 382 | this.controls.enablePan = false; 383 | this.controls.enableZoom = false; 384 | this.controls.enableRotate = false; 385 | this.renderer.domElement.style.cursor = 'move'; 386 | window.removeEventListener('mousemove', this.onMouseMove); 387 | window.addEventListener('mousemove', this.drag.mousemove); 388 | window.addEventListener('mouseup', this.drag.mouseup); 389 | }, 390 | end: () => { 391 | this.scene.remove(this.drag.ground); 392 | this.controls.enablePan = true; 393 | this.controls.enableZoom = true; 394 | this.controls.enableRotate = true; 395 | window.removeEventListener('mousemove', this.drag.mousemove); 396 | window.removeEventListener('mouseup', this.drag.mouseup); 397 | window.addEventListener('mousemove', this.onMouseMove); 398 | }, 399 | mousemove: (event: MouseEvent) => { 400 | this.updateMouseAndRay(event); 401 | const intersects = this.raycaster.intersectObject(this.drag.ground); // 鼠标与拖动地面的相交情况 402 | if (intersects.length) { 403 | this.drag.updateClipBox(intersects[0].point); 404 | } 405 | }, 406 | mouseup: () => { 407 | this.drag.end(); 408 | }, 409 | initGround: () => { // 初始化鼠标拖动时所在的平面(称之为地面,即 ground ) 410 | const normals: any = { 411 | 'x1': new Vector3(-1, 0, 0), 412 | 'x2': new Vector3(1, 0, 0), 413 | 'y1': new Vector3(0, -1, 0), 414 | 'y2': new Vector3(0, 1, 0), 415 | 'z1': new Vector3(0, 0, -1), 416 | 'z2': new Vector3(0, 0, 1) 417 | } 418 | if (['x1', 'x2'].includes(this.drag.axis)) { 419 | this.drag.point.setX(0); 420 | } else if (['y1', 'y2'].includes(this.drag.axis)) { 421 | this.drag.point.setY(0); 422 | } else if (['z1', 'z2'].includes(this.drag.axis)) { 423 | this.drag.point.setZ(0); 424 | } 425 | this.drag.ground.position.copy(this.drag.point); 426 | const newNormal = this.camera.position.clone(). 427 | sub(this.camera.position.clone().projectOnVector(normals[this.drag.axis])) 428 | .add(this.drag.point); // 转换得到平面的法向量 429 | this.drag.ground.lookAt(newNormal); 430 | this.scene.add(this.drag.ground); 431 | }, 432 | updateClipBox: (point: Vector3) => { // 更新剖切盒,进行剖切 433 | 434 | // 设置剖切盒的最低点和最高点 435 | const minSize = 2; // 剖切盒的最小大小 436 | switch (this.drag.axis) { 437 | case 'y2': // 上 438 | this.high.setY(Math.max(this.low.y + minSize, Math.min(this.high_init.y, point.y))); 439 | break; 440 | case 'y1': // 下 441 | this.low.setY(Math.max(this.low_init.y, Math.min(this.high.y - minSize, point.y))); 442 | break; 443 | case 'x1': // 左 444 | this.low.setX(Math.max(this.low_init.x, Math.min(this.high.x - minSize, point.x))); 445 | break; 446 | case 'x2': // 右 447 | this.high.setX(Math.max(this.low.x + minSize, Math.min(this.high_init.x, point.x))); 448 | break; 449 | case 'z2': // 前 450 | this.high.setZ(Math.max(this.low.z + minSize, Math.min(this.high_init.z, point.z))); 451 | break; 452 | case 'z1': // 后 453 | this.low.setZ(Math.max(this.low_init.z, Math.min(this.high.z - minSize, point.z))); 454 | break; 455 | } 456 | 457 | // 更新剖切盒的剖切平面、顶点、面和边线 458 | this.initPlanes(); 459 | this.initVertices(); 460 | this.faces.forEach((face: any) => { 461 | face.geometry.verticesNeedUpdate = true; 462 | face.geometry.computeBoundingBox(); 463 | face.geometry.computeBoundingSphere(); 464 | }) 465 | this.lines.forEach((line: any) => { 466 | line.geometry.verticesNeedUpdate = true; 467 | }) 468 | 469 | // 更新覆盖盒 470 | this.updateCapBoxList(); 471 | } 472 | } 473 | 474 | } 475 | 476 | 477 | /**类:用于构造剖切盒的边线 */ 478 | class BoxLine extends LineSegments { 479 | 480 | // (1)基本数据 481 | private normalMaterial = new LineBasicMaterial({ color: 0xe1f2fb }); // 边线的常态 482 | private activeMaterial = new LineBasicMaterial({ color: 0x00ffff }); // 边线的活跃态 483 | 484 | /** 485 | * (2)构造函数 486 | * @param vertices 边线的 2 个点 487 | * @param faces 边线涉及的 2 个面 488 | */ 489 | constructor(vertices: Array, faces: Array) { 490 | super(); 491 | faces.forEach(face => { face.lines.push(this) }); // 保存面和边线的关系 492 | this.geometry = new Geometry(); 493 | this.geometry.vertices.push(...vertices); 494 | this.material = this.normalMaterial; 495 | } 496 | 497 | /** 498 | * (3)活跃边线 499 | * @param isActive 是否活跃 500 | */ 501 | setActive(isActive: boolean) { 502 | this.material = isActive ? this.activeMaterial : this.normalMaterial; 503 | } 504 | } 505 | 506 | 507 | /**类:用于构造剖切盒的面 */ 508 | class BoxFace extends Mesh { 509 | 510 | // (1)基本数据 511 | axis: string; 512 | lines: Array = []; // 面涉及的 4 条边线 513 | backFace: Mesh; // 面的背面,用来展示 514 | 515 | /** 516 | * (2)构造函数 517 | * @param axis 面的轴线 518 | * @param vertices 面的 4 个点 519 | */ 520 | constructor(axis: string, vertices: Array) { 521 | super(); 522 | this.axis = axis; 523 | this.lines = []; 524 | this.geometry = new Geometry(); 525 | this.geometry.vertices.push(...vertices); 526 | this.geometry.faces.push(new Face3(0, 3, 2), new Face3(0, 2, 1)); 527 | this.geometry.computeVertexNormals(); 528 | this.geometry.computeFaceNormals(); 529 | this.material = new MeshBasicMaterial({ colorWrite: false, depthWrite: false }); 530 | const backMaterial = new MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.2, side: BackSide }); 531 | this.backFace = new Mesh(this.geometry, backMaterial); 532 | } 533 | 534 | /** 535 | * (3)活跃面,即活跃相关边线 536 | * @param isActive 是否活跃 537 | */ 538 | setActive(isActive: boolean) { 539 | this.lines.forEach(line => { line.setActive(isActive) }); 540 | } 541 | 542 | } -------------------------------------------------------------------------------- /src/scripts/lut.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "three"; 2 | 3 | export class Lut { 4 | 5 | lut: any = []; 6 | map: any = []; 7 | n = 256; 8 | minV = 0; 9 | maxV = 1; 10 | colorMapKeywords: any = { 11 | "rainbow": [[0.0, 0x0000FF], [0.2, 0x00FFFF], [0.5, 0x00FF00], [0.8, 0xFFFF00], [1.0, 0xFF0000]], 12 | "cooltowarm": [[0.0, 0x3C4EC2], [0.2, 0x9BBCFF], [0.5, 0xDCDCDC], [0.8, 0xF6A385], [1.0, 0xB40426]], 13 | "blackbody": [[0.0, 0x000000], [0.2, 0x780000], [0.5, 0xE63200], [0.8, 0xFFFF00], [1.0, 0xFFFFFF]], 14 | "grayscale": [[0.0, 0x000000], [0.2, 0x404040], [0.5, 0x7F7F80], [0.8, 0xBFBFBF], [1.0, 0xFFFFFF]] 15 | }; 16 | 17 | constructor(colormap: string, numberofcolors: number) { 18 | this.lut = []; 19 | this.setColorMap(colormap, numberofcolors); 20 | } 21 | 22 | set(value: Lut) { 23 | if (value instanceof Lut) { 24 | this.copy(value); 25 | } 26 | return this; 27 | } 28 | 29 | setMin(min: number) { 30 | this.minV = min; 31 | return this; 32 | } 33 | 34 | setMax(max: number) { 35 | this.maxV = max; 36 | return this; 37 | } 38 | 39 | setColorMap(colormap: string, numberofcolors: number) { 40 | 41 | this.map = this.colorMapKeywords[colormap] || this.colorMapKeywords.rainbow; 42 | this.n = numberofcolors || 32; 43 | 44 | var step = 1.0 / this.n; 45 | 46 | this.lut.length = 0; 47 | for (var i = 0; i <= 1; i += step) { 48 | 49 | for (var j = 0; j < this.map.length - 1; j++) { 50 | 51 | if (i >= this.map[j][0] && i < this.map[j + 1][0]) { 52 | 53 | var min = this.map[j][0]; 54 | var max = this.map[j + 1][0]; 55 | 56 | var minColor = new Color(this.map[j][1]); 57 | var maxColor = new Color(this.map[j + 1][1]); 58 | 59 | var color = minColor.lerp(maxColor, (i - min) / (max - min)); 60 | 61 | this.lut.push(color); 62 | 63 | } 64 | 65 | } 66 | 67 | } 68 | 69 | return this; 70 | 71 | } 72 | 73 | copy(lut: Lut) { 74 | 75 | this.lut = lut.lut; 76 | this.map = lut.map; 77 | this.n = lut.n; 78 | this.minV = lut.minV; 79 | this.maxV = lut.maxV; 80 | 81 | return this; 82 | 83 | } 84 | 85 | getColor(alpha: number) { 86 | 87 | if (alpha <= this.minV) { 88 | 89 | alpha = this.minV; 90 | 91 | } else if (alpha >= this.maxV) { 92 | 93 | alpha = this.maxV; 94 | 95 | } 96 | 97 | alpha = (alpha - this.minV) / (this.maxV - this.minV); 98 | 99 | var colorPosition = Math.round(alpha * this.n); 100 | colorPosition == this.n ? colorPosition -= 1 : colorPosition; 101 | 102 | return this.lut[colorPosition]; 103 | 104 | } 105 | 106 | addColorMap(colormapName: string, arrayOfColors: any) { 107 | 108 | this.colorMapKeywords[colormapName] = arrayOfColors; 109 | 110 | } 111 | 112 | createCanvas() { 113 | 114 | var canvas = document.createElement('canvas'); 115 | canvas.width = 1; 116 | canvas.height = this.n; 117 | 118 | this.updateCanvas(canvas); 119 | 120 | return canvas; 121 | 122 | } 123 | 124 | updateCanvas(canvas: any) { 125 | 126 | var ctx = canvas.getContext('2d', { alpha: false }); 127 | 128 | var imageData = ctx.getImageData(0, 0, 1, this.n); 129 | 130 | var data = imageData.data; 131 | 132 | var k = 0; 133 | 134 | var step = 1.0 / this.n; 135 | 136 | for (var i = 1; i >= 0; i -= step) { 137 | 138 | for (var j = this.map.length - 1; j >= 0; j--) { 139 | 140 | if (i < this.map[j][0] && i >= this.map[j - 1][0]) { 141 | 142 | var min = this.map[j - 1][0]; 143 | var max = this.map[j][0]; 144 | 145 | var minColor = new Color(this.map[j - 1][1]); 146 | var maxColor = new Color(this.map[j][1]); 147 | 148 | var color = minColor.lerp(maxColor, (i - min) / (max - min)); 149 | 150 | data[k * 4] = Math.round(color.r * 255); 151 | data[k * 4 + 1] = Math.round(color.g * 255); 152 | data[k * 4 + 2] = Math.round(color.b * 255); 153 | data[k * 4 + 3] = 255; 154 | 155 | k += 1; 156 | 157 | } 158 | 159 | } 160 | 161 | } 162 | 163 | ctx.putImageData(imageData, 0, 0); 164 | 165 | return canvas; 166 | 167 | } 168 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es5", 6 | "outDir": "dist", 7 | "allowJs": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "sourceMap": true, 11 | "stripInternal": true, 12 | "importHelpers": true, 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ] 16 | }, 17 | "files": [ 18 | "src/index.ts" 19 | ], 20 | "exclude": [ 21 | "dist", 22 | "node_modules", 23 | "webpack.config.js" 24 | ] 25 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | index: './src/index.ts' 9 | }, 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | filename: "[name].[hash].bundle.js" 13 | }, 14 | resolve: { 15 | extensions: ['.ts', '.js', '.css'] 16 | }, 17 | module: { 18 | rules: [ 19 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 20 | { test: /\.ts$/, use: 'ts-loader' } 21 | ] 22 | }, 23 | plugins: [ 24 | new webpack.HotModuleReplacementPlugin(), 25 | new HtmlWebpackPlugin({ 26 | template: './src/index.html' 27 | }), 28 | new CopyWebpackPlugin([ 29 | { from: './src/assets', to: 'assets' } 30 | ]) 31 | ], 32 | devtool: 'cheap-module-eval-source-map', 33 | devServer: { 34 | contentBase: path.join(__dirname, "dist"), 35 | compress: true, 36 | port: 9000 37 | } 38 | } --------------------------------------------------------------------------------