├── .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 | 
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 |
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 | }
--------------------------------------------------------------------------------