feat(All):Initial

This commit is contained in:
2025-10-04 23:36:07 +08:00
commit 2b4e5d2668
1321 changed files with 415958 additions and 0 deletions
+811
View File
@@ -0,0 +1,811 @@
import * as THREE from "three";
import {CSS2DObject} from "three/examples/jsm/renderers/CSS2DRenderer.js";
import {useDispatchSignal} from "@/hooks";
import Viewer from "@/core/viewer/Viewer";
export interface MeasureEventMap{
/**
* 完成绘制触发
*/
complete: {object: THREE.Group};
}
export enum MeasureMode {
Distance = "Distance",
Area = "Area",
Angle = "Angle"
}
let pdFn, pmFn, puFn, kdFn;
/**
* Measure class
*/
class Measure extends THREE.EventDispatcher<MeasureEventMap>{
static LINE_MATERIAL = new THREE.LineBasicMaterial({
color: 0xE63C17,
linewidth: 2,
opacity: 0.9,
transparent: true,
side: THREE.DoubleSide,
depthWrite: false,
depthTest: false
});
static MESH_MATERIAL = new THREE.MeshBasicMaterial({
color: 0x87cefa,
transparent: true,
opacity: 0.7,
side: THREE.DoubleSide,
depthWrite: false,
depthTest: false
});
static MAX_DISTANCE = 500; //当相交物体的距离太远时,忽略它
static OBJ_NAME = "object_for_measure";
static LABEL_NAME = "label_for_measure";
public isCompleted = true; // 测量操作是否完成
public isClose = true; // 测量操作是否已关闭(全销毁)
public mode: MeasureMode;
protected scene: THREE.Scene;
protected spriteMaterial?: THREE.SpriteMaterial;
protected raycaster?: THREE.Raycaster;
protected mouseMoved = false;
protected polyline?: THREE.Line; // 用户在测量时绘制的线的当前实例
protected faces?: THREE.Mesh; // 用于测量面积的当前实例
protected curve?: THREE.Line; // 用弧线表示角度
protected tempPointMarker?: THREE.Sprite; // 用于存储临时点
protected tempLine?: THREE.Line; // 用于存储临时线条,用于在鼠标移动时绘制线条/区域/角度
protected tempLabel?: CSS2DObject; // 用于在鼠标移动时存储临时标签,只有测量距离时才有
protected pointArray: THREE.Vector3[] = []; // 存储点
protected lastClickTime?: number; //保存上次点击时间,以便检测双击事件
protected viewer:Viewer;
// 所有测绘内容组
public measureGroup: THREE.Group;
// 当前测绘内容组
protected group: THREE.Group;
constructor(viewer:Viewer, mode: MeasureMode = MeasureMode.Distance) {
super();
this.mode = mode;
this.scene = viewer.sceneHelpers;
this.viewer = viewer;
// 初始化group
this.measureGroup = new THREE.Group();
this.measureGroup.name = `measure_group`;
this.group = new THREE.Group();
this.scene.add(this.measureGroup);
this.viewer.modules.dragControl.setMeasureInstance(this);
}
get domElement(): HTMLCanvasElement{
return this.viewer.renderer.domElement;
}
get canvas(): HTMLCanvasElement {
return this.domElement as HTMLCanvasElement;
}
addEvent() {
pdFn = this.mousedown.bind(this);
this.canvas.addEventListener("pointerdown", pdFn);
pmFn = this.mousemove.bind(this);
this.canvas.addEventListener("pointermove", pmFn);
puFn = this.mouseup.bind(this);
this.canvas.addEventListener("pointerup", puFn);
kdFn = this.keydown.bind(this);
window.addEventListener("keydown", kdFn);
}
removeEvent() {
this.canvas.removeEventListener("pointerdown", pdFn);
pdFn = undefined;
this.canvas.removeEventListener("pointermove", pmFn);
pmFn = undefined;
this.canvas.removeEventListener("pointerup", puFn);
puFn = undefined;
window.removeEventListener("keydown", kdFn);
kdFn = undefined;
}
// 开始测量
open() {
this.addEvent();
if (this.isClose) {
this.raycaster = new THREE.Raycaster();
}
// 重置
this.group = new THREE.Group();
this.group.name = `${Measure.OBJ_NAME}_group`;
this.group.userData = {
mode: this.mode
}
this.measureGroup.add(this.group);
// 当次绘制点
this.pointArray = [];
// 测量距离、面积和角度需要折线
this.polyline = this.createLine();
this.group.add(this.polyline as THREE.Object3D);
if (this.mode === MeasureMode.Area) {
this.faces = this.createFaces();
this.group.add(this.faces as THREE.Object3D);
}
this.isCompleted = false;
this.isClose = false;
this.domElement.style.cursor = "crosshair";
// 禁用拖拽控制器
this.viewer.modules.dragControl.dragControls.enabled = false;
}
// 重绘
redraw(point: THREE.Sprite) {
// 当次绘制点
this.pointArray = [];
(point.parent as THREE.Group).children.forEach(child => {
switch (child.userData.type) {
case "measure-marker":
// 当前点正在操作,不加入
if(child.uuid !== point.uuid) {
this.pointArray[child.userData.pointIndex] = child.userData.point;
}else{
this.tempPointMarker = child as THREE.Sprite;
}
break;
case "line":
this.polyline = child as THREE.Line;
this.tempLine = this.createLine();
this.scene.add(this.tempLine as THREE.Object3D);
break;
case "faces":
this.faces = child as THREE.Mesh;
break;
case "curve":
this.curve = child as THREE.Line;
break;
}
})
// 重写move事件
pmFn = this.redrawMousemove.bind(this);
this.canvas.addEventListener("pointermove", pmFn);
this.group = point.parent as THREE.Group;
this.isCompleted = false;
this.mode = this.group.userData.mode;
}
/**
* 结束测量,清空所有结果
*/
clear() {
this.removeEvent();
this.clearTemp();
this.measureGroup.children.forEach(g => {
for (let i = g.children.length - 1; i >= 0; i--) {
const c = g.children[i];
if(c.userData.type == "measure-marker"){
// 从拖拽控制器移除
this.viewer.modules.dragControl.setDragObjects([c], "remove");
}
g.remove(c)
}
})
this.measureGroup.remove(...this.measureGroup.children);
this.polyline = undefined;
this.faces = undefined;
this.curve = undefined;
this.pointArray = [];
this.raycaster = undefined;
this.domElement.style.cursor = "";
this.isClose = true;
useDispatchSignal("sceneGraphChanged")
}
/**
* 新版本threejs中,BufferGeometry.setFromPoints方法不在支持添加点位置,需要手动设置顶点属性
*/
setFromPoints(geo: THREE.BufferGeometry, points: THREE.Vector3[]){
const position:number[] = [];
for ( let i = 0, l = points.length; i < l; i++) {
const point = points[ i ];
position.push(point.x, point.y, point.z);
}
geo.setAttribute('position', new THREE.Float32BufferAttribute(position, 3));
}
/**
* 初始化点标记材料
*/
initPointMarkerMaterial() {
const markerTexture = new THREE.TextureLoader().load("/static/images/logo/logo.png");
this.spriteMaterial = new THREE.SpriteMaterial({
map: markerTexture,
depthTest: false, // 深度测试
depthWrite: false, // 深度写入
sizeAttenuation: false,
transparent: true,
opacity: 0.9
});
}
// 创建点标记
createPointMarker(position?: THREE.Vector3): THREE.Sprite {
if (!this.spriteMaterial) {
this.initPointMarkerMaterial();
}
const p = position;
const scale = 0.012;
const obj = new THREE.Sprite(this.spriteMaterial);
obj.scale.set(scale, scale, scale);
if (p) {
obj.position.set(p.x, p.y, p.z);
}
obj.name = Measure.OBJ_NAME;
obj.userData = {
mode: this.mode,
type: "measure-marker",
}
return obj;
}
/**
* Creates THREE.Line
*/
private createLine(): THREE.Line {
const geom = new THREE.BufferGeometry();
const obj = new THREE.Line(geom, Measure.LINE_MATERIAL);
obj.frustumCulled = false;
obj.name = Measure.OBJ_NAME;
obj.userData = {
type: "line",
}
return obj;
}
/**
* Creates THREE.Mesh
*/
private createFaces() {
const geom = new THREE.BufferGeometry();
const obj = new THREE.Mesh(geom, Measure.MESH_MATERIAL);
obj.frustumCulled = false;
obj.name = Measure.OBJ_NAME;
obj.userData = {
// 将点存储到userData中
vertices: [],
type: "faces",
}
return obj;
}
// 清除临时信息
clearTemp() {
this.tempPointMarker && this.scene.remove(this.tempPointMarker);
this.tempLine && this.scene.remove(this.tempLine as THREE.Object3D);
this.tempLabel && this.scene.remove(this.tempLabel);
this.tempPointMarker = undefined;
this.tempLine = undefined;
this.tempLabel = undefined;
}
// 完成绘制,不清空结果
complete() {
if (this.isCompleted) return;
useDispatchSignal("sceneGraphChanged")
let clearPoints = false;
let clearPolyline = false;
// 为了测量面积,我们需要制作一个接近的表面,然后添加面积标签
const count = this.pointArray.length;
if (this.mode === MeasureMode.Area && this.polyline) {
if (count > 2) {
const p0 = this.pointArray[0];
this.setFromPoints(this.polyline.geometry, [...this.pointArray, p0]);
// 计算面积
const area = this.calculateArea(this.pointArray);
const label = `${this.numberToString(area)} ${this.getUnitString()}`;
const p = this.getBarycenter(this.pointArray);
const labelObj = this.createLabel(label);
labelObj.position.set(p.x, p.y, p.z);
labelObj.element.innerHTML = label;
this.group.add(labelObj);
} else {
clearPoints = true;
clearPolyline = true;
}
}
if (this.mode === MeasureMode.Distance) {
if (count < 2) {
clearPoints = true;
}
}
if (this.mode === MeasureMode.Angle && this.polyline) {
if (count >= 3) {
const p0 = this.pointArray[0];
const p1 = this.pointArray[1];
const p2 = this.pointArray[2];
const dir0 = new THREE.Vector3(p0.x - p1.x, p0.y - p1.y, p0.z - p1.z).normalize();
const dir1 = this.getAngleBisector(p0, p1, p2);
const dir2 = new THREE.Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z).normalize();
const angle = this.calculateAngle(p0, p1, p2);
const label = `${this.numberToString(angle)} ${this.getUnitString()}`;
const distance = Math.min(p0.distanceTo(p1), p2.distanceTo(p1));
let d = distance * 0.3; // distance from label to p1
let p = p1.clone().add(new THREE.Vector3(dir1.x * d, dir1.y * d, dir1.z * d)); // label's position
const labelObj = this.createLabel(label);
labelObj.position.set(p.x, p.y, p.z);
labelObj.element.innerHTML = label;
this.group.add(labelObj);
d = distance * 0.2; // 弧到p1的距离
p = p1.clone().add(new THREE.Vector3(dir1.x * d, dir1.y * d, dir1.z * d)); // 圆弧中间位置
const arcP0 = p1.clone().add(new THREE.Vector3(dir0.x * d, dir0.y * d, dir0.z * d));
const arcP2 = p1.clone().add(new THREE.Vector3(dir2.x * d, dir2.y * d, dir2.z * d));
this.curve = this.createCurve(arcP0, p, arcP2);
// 添加弧
this.group.add(this.curve as THREE.Object3D);
} else {
clearPoints = true;
clearPolyline = true;
}
}
// 无效的情况,清除此次的无用的对象
if (clearPoints) {
// 从this.measureGroup移除
this.measureGroup.remove(this.group);
}
if (clearPolyline && this.polyline) {
this.group.remove(this.polyline as THREE.Object3D);
this.polyline = undefined;
}
this.isCompleted = true;
this.domElement.style.cursor = "";
this.clearTemp();
useDispatchSignal("sceneGraphChanged");
this.removeEvent();
// 启用拖拽控制器
this.viewer.modules.dragControl.dragControls.enabled = true;
this.dispatchEvent({type:"complete",object:this.group})
}
// 清除当前group label
clearCurrentLabel() {
for (let i = this.group.children.length - 1; i >=0 ; i--) {
const c = this.group.children[i];
if(c.userData.type === "label"){
this.group.remove(c);
}
}
}
// 获取按下对应三维位置
getClosestIntersection(e: MouseEvent){
const _point = new THREE.Vector2();
_point.x = e.offsetX / this.viewer.renderer.domElement.offsetWidth;
_point.y = e.offsetY / this.viewer.renderer.domElement.offsetHeight;
const intersects = this.viewer.getIntersects(_point);
if (intersects && intersects.length > 0) {
if (intersects.length > 0 && intersects[0].distance < Measure.MAX_DISTANCE) {
return intersects[0].point;
}
}
return null;
}
// 重绘监听鼠标移动
redrawMousemove(e: MouseEvent) {
let point = this.getClosestIntersection(e);
if (!point && this.tempPointMarker) {
this.tempPointMarker.position.set(this.tempPointMarker.userData.point.x, this.tempPointMarker.userData.point.y, this.tempPointMarker.userData.point.z);
return;
}
if (!point || !this.tempPointMarker) return;
// 在鼠标移动时绘制临时点
this.tempPointMarker.position.set(point.x, point.y, point.z);
this.tempPointMarker.userData.point = point;
// 当前点的索引
const cIndex = this.tempPointMarker.userData.pointIndex;
// 移动时绘制临时线
if (this.pointArray.length > 0) {
const line = this.tempLine || this.createLine();
const geom = line.geometry;
let startPoint = this.pointArray[cIndex + 1];
let lastPoint = this.pointArray[cIndex - 1];
// 如果是面积测量,且当前点是最后一个点或者第一个点
// 则需要重置其中一个点,才能有两条线拖动效果
if(this.mode === MeasureMode.Area){
if(!lastPoint){
lastPoint = this.pointArray[this.pointArray.length - 1];
}else if(!startPoint){
startPoint = this.pointArray[0];
}
}
if (startPoint && lastPoint) {
this.setFromPoints(geom, [lastPoint, point, startPoint]);
} else {
this.setFromPoints(geom,[startPoint || lastPoint, point]);
}
}
}
// 重绘完成
redrawComplete() {
if(!this.tempPointMarker) return;
const point = this.tempPointMarker.userData.point;
this.pointArray[this.tempPointMarker.userData.pointIndex] = point;
const count = this.pointArray.length;
if (this.polyline) {
this.setFromPoints(this.polyline.geometry,this.pointArray);
// 如果是距离测量,则清除group中已有的label,再重新创建
if (this.mode === MeasureMode.Distance && count > 1) {
this.clearCurrentLabel();
// 绘制label
for (let i = 0; i < count - 1; i++) {
const p0 = this.pointArray[i];
const p1 = this.pointArray[i + 1];
if(!p0 || !p1) continue;
const dist = p0.distanceTo(p1);
const label = `${this.numberToString(dist)} ${this.getUnitString()}`;
const position = new THREE.Vector3((p0.x + p1.x) / 2, (p0.y + p1.y) / 2, (p0.z + p1.z) / 2);
const labelObj = this.createLabel(label);
labelObj.position.set(position.x, position.y, position.z);
labelObj.element.innerHTML = label;
this.group.add(labelObj);
}
}
}
// 面积测量
if (this.mode === MeasureMode.Area && this.faces) {
const geom = this.faces.geometry as THREE.BufferGeometry;
const vertices = this.faces.userData.vertices;
// vertices.push(point);
vertices[this.tempPointMarker.userData.pointIndex] = point;
this.setFromPoints(geom,vertices);
const len = vertices.length;
if (len > 2) {
const indexArray:number[] = [];
for (let i = 1; i < len - 1; ++i) {
indexArray.push(0, i, i + 1);
}
geom.setIndex(indexArray);
geom.computeVertexNormals();
}
// 移除原来的label,新的label会在complete中创建
this.clearCurrentLabel();
}
// 角度测量
if (this.mode === MeasureMode.Angle && this.curve) {
// 清除弧跟原来的label 会在complete中创建
this.group.remove(this.curve as THREE.Object3D);
this.clearCurrentLabel();
}
this.complete();
}
mousedown = () => {
this.mouseMoved = false;
};
// 鼠标移动,创建对应的临时点与线
mousemove = (e: MouseEvent) => {
if(this.isCompleted) return;
this.mouseMoved = true;
const point = this.getClosestIntersection(e);
if (!point) {
return;
}
// 在鼠标移动时绘制临时点
if (this.tempPointMarker) {
this.tempPointMarker.position.set(point.x, point.y, point.z);
} else {
this.tempPointMarker = this.createPointMarker(point);
this.scene.add(this.tempPointMarker);
}
// 移动时绘制临时线
if (this.pointArray.length > 0) {
const p0 = this.pointArray[this.pointArray.length - 1]; // 获取最后一个点
const line = this.tempLine || this.createLine();
const geom = line.geometry;
const startPoint = this.pointArray[0];
const lastPoint = this.pointArray[this.pointArray.length - 1];
if (this.mode === MeasureMode.Area) {
this.setFromPoints(geom,[lastPoint, point, startPoint]);
} else {
this.setFromPoints(geom,[lastPoint, point]);
}
if (this.mode === MeasureMode.Distance) {
const dist = p0.distanceTo(point);
const label = `${this.numberToString(dist)} ${this.getUnitString()}`;
const position = new THREE.Vector3((point.x + p0.x) / 2, (point.y + p0.y) / 2, (point.z + p0.z) / 2);
this.addOrUpdateTempLabel(label, position);
}
// tempLine 只需添加到场景一次
if (!this.tempLine) {
this.scene.add(line as THREE.Object3D);
this.tempLine = line;
}
}
useDispatchSignal("sceneGraphChanged")
};
mouseup = (e: MouseEvent) => {
// 如果mouseMoved是true,那么它可能在移动,而不是点击
if (!this.mouseMoved) {
// 右键点击表示完成绘图操作
if (e.button === 2) {
this.complete();
} else if (e.button === 0) { // 左键点击表示添加点
this.onMouseClicked(e);
}
}
};
onMouseClicked = (e: MouseEvent) => {
if (!this.raycaster || !this.viewer.camera || !this.scene || this.isCompleted) {
return;
}
const point =this.getClosestIntersection(e);
if (!point) {
return;
}
// 双击触发两次点击事件,我们需要避免这里的第二次点击
const now = Date.now();
if (this.lastClickTime && (now - this.lastClickTime < 100)) return;
this.lastClickTime = now;
this.pointArray.push(point);
const count = this.pointArray.length;
const marker = this.createPointMarker(point);
marker.userData.point = point;
marker.userData.pointIndex = count - 1;
this.group.add(marker);
// 把点加入拖拽控制器
this.viewer.modules.dragControl.setDragObjects([marker], "push");
if (this.polyline) {
this.setFromPoints(this.polyline.geometry, this.pointArray);
if (this.tempLabel && count > 1) {
const p0 = this.pointArray[count - 2];
this.tempLabel.position.set((p0.x + point.x) / 2, (p0.y + point.y) / 2, (p0.z + point.z) / 2);
this.group.add(this.tempLabel);
// 创建距离测量线时,此处的 临时label 将作为正式的使用,不在this.clearTemp()中清除,故置为undefined
this.tempLabel = undefined;
}
}
if (this.mode === MeasureMode.Area && this.faces) {
const geom = this.faces.geometry as THREE.BufferGeometry;
const vertices = this.faces.userData.vertices;
vertices.push(point);
this.setFromPoints(geom,vertices);
const len = vertices.length;
if (len > 2) {
const indexArray:number[] = [];
for (let i = 1; i < len - 1; ++i) {
indexArray.push(0, i, i + 1);
}
geom.setIndex(indexArray);
geom.computeVertexNormals();
this.clearCurrentLabel();
const p0 = this.pointArray[0];
this.setFromPoints(geom, [...this.pointArray, p0]);
// 计算面积
const area = this.calculateArea(this.pointArray);
const label = `${this.numberToString(area)} ${this.getUnitString()}`;
const p = this.getBarycenter(this.pointArray);
const labelObj = this.createLabel(label);
labelObj.position.set(p.x, p.y, p.z);
labelObj.element.innerHTML = label;
this.group.add(labelObj);
}
}
// 创建角度测量时,三个点完成
if (this.mode === MeasureMode.Angle && this.pointArray.length % 3 === 0) {
this.complete();
}
useDispatchSignal("sceneGraphChanged");
};
keydown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
this.complete();
}
};
/**
* 添加或更新临时标签和位置
*/
addOrUpdateTempLabel(label: string, position: THREE.Vector3) {
if (!this.tempLabel) {
this.tempLabel = this.createLabel(label);
this.scene.add(this.tempLabel);
}
this.tempLabel.position.set(position.x, position.y, position.z);
this.tempLabel.element.innerHTML = label;
}
/**
* 创建标签
*/
createLabel(text: string): CSS2DObject {
const div = document.createElement("div");
div.className = 'css2dObjectLabel';
div.innerHTML = text;
div.style.padding = "5px 8px";
div.style.color = "#fff";
div.style.fontSize = "14px";
div.style.position = "absolute";
div.style.backgroundColor = "rgba(25, 25, 25, 0.3)";
div.style.borderRadius = "12px";
div.style.top = "0px";
div.style.left = "0px";
// div.style.pointerEvents = 'none' //避免HTML元素影响场景的鼠标事件
const obj = new CSS2DObject(div);
obj.name = Measure.LABEL_NAME;
obj.userData = {
type: "label"
}
return obj;
}
/**
* 创建圆弧曲线以表示角度
*/
createCurve(p0: THREE.Vector3, p1: THREE.Vector3, p2: THREE.Vector3) {
const curve = new THREE.QuadraticBezierCurve3(p0, p1, p2);
const points = curve.getPoints(4);
const geometry = new THREE.BufferGeometry();
this.setFromPoints(geometry,points)
const obj = new THREE.Line(geometry, Measure.LINE_MATERIAL);
obj.name = Measure.OBJ_NAME;
obj.userData = {
type: "curve"
}
return obj;
}
/**
* 计算区域
* TODO: 对于凹多边形,数值不对,需要修正
* @param points
*/
calculateArea(points: THREE.Vector3[]) {
let area = 0;
for (let i = 0, j = 1, k = 2; k < points.length; j++, k++) {
const a = points[i].distanceTo(points[j]);
const b = points[j].distanceTo(points[k]);
const c = points[k].distanceTo(points[i]);
const p = (a + b + c) / 2;
area += Math.sqrt(p * (p - a) * (p - b) * (p - c));
}
return area;
}
/**
* 以度表示两条直线的夹角
*/
calculateAngle(startPoint: THREE.Vector3, middlePoint: THREE.Vector3, endPoint: THREE.Vector3) {
const p0 = startPoint;
const p1 = middlePoint;
const p2 = endPoint;
const dir0 = new THREE.Vector3(p0.x - p1.x, p0.y - p1.y, p0.z - p1.z);
const dir1 = new THREE.Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);
const angle = dir0.angleTo(dir1);
return angle * 180 / Math.PI; // convert to degree
}
/**
* 获取两条线的角平分线
*/
getAngleBisector(startPoint: THREE.Vector3, middlePoint: THREE.Vector3, endPoint: THREE.Vector3): THREE.Vector3 {
const p0 = startPoint;
const p1 = middlePoint;
const p2 = endPoint;
const dir0 = new THREE.Vector3(p0.x - p1.x, p0.y - p1.y, p0.z - p1.z).normalize();
const dir2 = new THREE.Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z).normalize();
return new THREE.Vector3(dir0.x + dir2.x, dir0.y + dir2.y, dir0.z + dir2.z).normalize(); // the middle direction between dir0 and dir2
}
/**
* 得到点的重心
*/
getBarycenter(points: THREE.Vector3[]): THREE.Vector3 {
const l = points.length;
let x = 0;
let y = 0;
let z = 0;
points.forEach(p => {
x += p.x;
y += p.y;
z += p.z
});
return new THREE.Vector3(x / l, y / l, z / l);
}
/**
* 获取距离、面积或角度的单位字符串
*/
getUnitString() {
if (this.mode === MeasureMode.Distance) return "m";
if (this.mode === MeasureMode.Area) return "m²";
if (this.mode === MeasureMode.Angle) return "°";
return "";
}
/**
* 将数字转换为具有适当分数数字的字符串
*/
numberToString(num: number) {
if (num < 0.0001) {
return num.toString();
}
let fractionDigits = 2;
if (num < 0.01) {
fractionDigits = 4;
} else if (num < 0.1) {
fractionDigits = 3;
}
return num.toFixed(fractionDigits);
}
dispose(): void {
this.clear();
}
}
export {Measure};