Skip to content

Commit fb7a4b3

Browse files
authored
Merge pull request #69 from weinibear/feature/pointCloudByThreejs
10w point cloud by threejs
2 parents d4209bd + d823d6e commit fb7a4b3

File tree

4 files changed

+270
-1
lines changed

4 files changed

+270
-1
lines changed

apps/admin/src/router/routes/compo.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ const CompoRoute: RouteObject = {
1919
orderNo: 6,
2020
},
2121
children: [
22+
{
23+
path: 'point-cloud',
24+
name: 'pointCloud',
25+
element: LazyLoad(lazy(() => import('@/views/compo/point-cloud'))),
26+
meta: {
27+
title: '10w点云',
28+
key: 'pointCloud',
29+
},
30+
},
2231
{
2332
path: 'image-upload',
2433
name: 'ImageUpload',
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { PointCloudSelect } from '@gbeata/three';
2+
3+
const PointCloud: React.FC = () => {
4+
return <PointCloudSelect />;
5+
};
6+
7+
export default PointCloud;

packages/three/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import MacBook from './src/components/mixin';
2+
import PointCloudSelect from './src/components/pointCloudSelect';
23

3-
export { MacBook };
4+
export { MacBook, PointCloudSelect };
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import { useEffect, useRef } from 'react';
2+
import * as THREE from 'three';
3+
4+
const POINT_COUNT = 100000;
5+
6+
interface Point {
7+
x: number;
8+
y: number;
9+
}
10+
11+
interface ThreeRef {
12+
scene: THREE.Scene | null;
13+
camera: THREE.PerspectiveCamera | null;
14+
renderer: THREE.WebGLRenderer | null;
15+
geometry: THREE.BufferGeometry | null;
16+
points: THREE.Points | null;
17+
}
18+
19+
export default function PointCloudSelect() {
20+
const containerRef = useRef<HTMLDivElement>(null);
21+
const selectBoxRef = useRef<HTMLDivElement>(null);
22+
23+
const selectingRef = useRef<boolean>(false);
24+
const startRef = useRef<Point>({ x: 0, y: 0 });
25+
26+
const threeRef = useRef<ThreeRef>({
27+
scene: null,
28+
camera: null,
29+
renderer: null,
30+
geometry: null,
31+
points: null,
32+
});
33+
34+
useEffect(() => {
35+
initThree();
36+
initPoints();
37+
initEvents();
38+
animate();
39+
40+
return cleanup;
41+
}, []);
42+
43+
function initThree() {
44+
const container = containerRef.current;
45+
if (!container) return;
46+
const { width, height } = container.getBoundingClientRect();
47+
48+
const scene = new THREE.Scene();
49+
scene.background = new THREE.Color(0x111111);
50+
51+
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
52+
camera.position.z = 300;
53+
54+
const renderer = new THREE.WebGLRenderer({ antialias: true });
55+
renderer.setSize(width, height);
56+
renderer.setPixelRatio(window.devicePixelRatio);
57+
58+
container.appendChild(renderer.domElement);
59+
60+
threeRef.current = { scene, camera, renderer, geometry: null, points: null };
61+
}
62+
63+
/* ---------- 创建 10W 点 ---------- */
64+
function initPoints() {
65+
const positions = new Float32Array(POINT_COUNT * 3);
66+
const colors = new Float32Array(POINT_COUNT * 3);
67+
68+
for (let i = 0; i < POINT_COUNT; i += 1) {
69+
const i3 = i * 3;
70+
positions[i3] = (Math.random() - 0.5) * 400;
71+
positions[i3 + 1] = (Math.random() - 0.5) * 400;
72+
positions[i3 + 2] = (Math.random() - 0.5) * 400;
73+
74+
colors[i3] = 0.6;
75+
colors[i3 + 1] = 0.6;
76+
colors[i3 + 2] = 0.6;
77+
}
78+
79+
const geometry = new THREE.BufferGeometry();
80+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
81+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
82+
83+
const material = new THREE.PointsMaterial({
84+
size: 2,
85+
vertexColors: true,
86+
});
87+
88+
const points = new THREE.Points(geometry, material);
89+
if (threeRef.current.scene) {
90+
threeRef.current.scene.add(points);
91+
}
92+
threeRef.current.geometry = geometry;
93+
threeRef.current.points = points;
94+
}
95+
96+
function onMouseDown(e: MouseEvent) {
97+
const box = selectBoxRef.current;
98+
if (!box || !containerRef.current) return;
99+
100+
e.preventDefault();
101+
selectingRef.current = true;
102+
103+
const p = getMouseInCanvas(e);
104+
startRef.current = p;
105+
106+
box.style.display = 'block';
107+
box.style.left = `${p.x}px`;
108+
box.style.top = `${p.y}px`;
109+
box.style.width = '0px';
110+
box.style.height = '0px';
111+
}
112+
113+
function onMouseMove(e: MouseEvent) {
114+
if (!selectingRef.current || !containerRef.current) return;
115+
const box = selectBoxRef.current;
116+
if (!box) return;
117+
118+
const start = startRef.current;
119+
const curr = getMouseInCanvas(e);
120+
121+
const minX = Math.min(start.x, curr.x);
122+
const minY = Math.min(start.y, curr.y);
123+
const w = Math.abs(curr.x - start.x);
124+
const h = Math.abs(curr.y - start.y);
125+
126+
box.style.left = `${minX}px`;
127+
box.style.top = `${minY}px`;
128+
box.style.width = `${w}px`;
129+
box.style.height = `${h}px`;
130+
}
131+
132+
function endSelection(e: MouseEvent) {
133+
if (!selectingRef.current) return;
134+
const box = selectBoxRef.current;
135+
if (!box || !containerRef.current) return;
136+
137+
selectingRef.current = false;
138+
box.style.display = 'none';
139+
140+
const end = getMouseInCanvas(e);
141+
selectPoints(startRef.current, end);
142+
}
143+
144+
function onMouseUp(e: MouseEvent) {
145+
endSelection(e);
146+
}
147+
148+
function initEvents() {
149+
const container = containerRef.current;
150+
if (!container) return;
151+
152+
// 监听放在 canvas 容器上,移动 / 抬起放在 window,保证拖拽过程中不会丢失事件
153+
container.addEventListener('mousedown', onMouseDown);
154+
window.addEventListener('mousemove', onMouseMove);
155+
window.addEventListener('mouseup', onMouseUp);
156+
}
157+
158+
function getMouseInCanvas(e: MouseEvent): Point {
159+
const rect = containerRef.current!.getBoundingClientRect();
160+
return {
161+
x: e.clientX - rect.left,
162+
y: e.clientY - rect.top,
163+
};
164+
}
165+
166+
/* ---------- 框选核心 ---------- */
167+
function selectPoints(start: Point, end: Point) {
168+
const { camera, geometry, renderer } = threeRef.current;
169+
if (!camera || !geometry || !renderer) return;
170+
const rect = renderer.domElement.getBoundingClientRect();
171+
172+
const minX = Math.min(start.x, end.x);
173+
const maxX = Math.max(start.x, end.x);
174+
const minY = Math.min(start.y, end.y);
175+
const maxY = Math.max(start.y, end.y);
176+
177+
const pos = geometry.attributes.position.array as Float32Array;
178+
const col = geometry.attributes.color.array as Float32Array;
179+
const v = new THREE.Vector3();
180+
181+
for (let i = 0; i < POINT_COUNT; i += 1) {
182+
const i3 = i * 3;
183+
184+
v.set(pos[i3], pos[i3 + 1], pos[i3 + 2]);
185+
v.project(camera);
186+
187+
// eslint-disable-next-line no-continue
188+
if (v.z < -1 || v.z > 1) continue;
189+
190+
const x = (v.x * 0.5 + 0.5) * rect.width;
191+
const y = (-v.y * 0.5 + 0.5) * rect.height;
192+
193+
const hit = x >= minX && x <= maxX && y >= minY && y <= maxY;
194+
195+
if (hit) {
196+
col[i3] = 1;
197+
col[i3 + 1] = 0;
198+
col[i3 + 2] = 0;
199+
} else {
200+
col[i3] = 0.6;
201+
col[i3 + 1] = 0.6;
202+
col[i3 + 2] = 0.6;
203+
}
204+
}
205+
206+
geometry.attributes.color.needsUpdate = true;
207+
}
208+
209+
function animate() {
210+
requestAnimationFrame(animate);
211+
const { scene, camera, renderer } = threeRef.current;
212+
if (scene && camera && renderer) {
213+
renderer.render(scene, camera);
214+
}
215+
}
216+
217+
function cleanup() {
218+
const container = containerRef.current;
219+
if (container) {
220+
container.removeEventListener('mousedown', onMouseDown);
221+
}
222+
window.removeEventListener('mousemove', onMouseMove);
223+
window.removeEventListener('mouseup', onMouseUp);
224+
225+
if (threeRef.current.renderer) {
226+
threeRef.current.renderer.dispose();
227+
// 同时把 canvas 从 DOM 中移除,避免内存泄露
228+
const canvas = threeRef.current.renderer.domElement;
229+
if (canvas && canvas.parentNode) {
230+
canvas.parentNode.removeChild(canvas);
231+
}
232+
}
233+
}
234+
235+
return (
236+
<div ref={containerRef} className='relative w-full h-full select-none'>
237+
<div
238+
ref={selectBoxRef}
239+
className='
240+
absolute
241+
hidden
242+
z-10
243+
border
244+
border-dashed
245+
border-sky-400
246+
bg-[rgba(100,150,255,0.15)]
247+
pointer-events-none
248+
'
249+
/>
250+
</div>
251+
);
252+
}

0 commit comments

Comments
 (0)