feat(All):Initial
This commit is contained in:
@@ -0,0 +1,559 @@
|
||||
import {
|
||||
Box3,
|
||||
Vector3,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Vector2,
|
||||
Raycaster,
|
||||
Group,
|
||||
LineSegments,
|
||||
LineBasicMaterial,
|
||||
Plane,
|
||||
PlaneGeometry,
|
||||
BackSide,
|
||||
BufferGeometry,
|
||||
Object3D
|
||||
} from "three";
|
||||
import {useAddSignal, useDispatchSignal, useRemoveSignal} from "@/hooks";
|
||||
import {isGroup} from "@/utils";
|
||||
import Viewer from "@/core/viewer/Viewer";
|
||||
import App from "@/core/app/App";
|
||||
|
||||
let objectSelectedFn;
|
||||
|
||||
/**
|
||||
* 盒剖切
|
||||
* @param viewer
|
||||
* @param controls
|
||||
*/
|
||||
class ClippedEdgesBox {
|
||||
// 最小剖切盒宽度
|
||||
static MIN_WIDTH = 0.05;
|
||||
|
||||
private viewer: Viewer;
|
||||
protected controls;
|
||||
|
||||
public isOpen: boolean = false;
|
||||
protected sectionBox?: Box3;
|
||||
protected lastSelected:Object3D | undefined = undefined;
|
||||
|
||||
constructor(viewer:Viewer) {
|
||||
this.viewer = viewer;
|
||||
this.controls = viewer.modules.controls;
|
||||
|
||||
// 开启模型对象的局部剪裁平面功能. 如果不设置为true,设置剪裁平面的模型不会被剪裁
|
||||
this.viewer.renderer.localClippingEnabled = true;
|
||||
|
||||
objectSelectedFn = this.objectSelected.bind(this);
|
||||
}
|
||||
|
||||
get domElement(){
|
||||
return this.viewer.renderer.domElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果在构造函数中没有分配sectionBox,那么在这里设置它
|
||||
*/
|
||||
protected setSectionBox() {
|
||||
this.sectionBox = new Box3();
|
||||
|
||||
if (!App.selected) {
|
||||
this.lastSelected = undefined;
|
||||
this.sectionBox.expandByObject(this.viewer.scene)
|
||||
} else {
|
||||
this.lastSelected = App.selected;
|
||||
this.sectionBox?.expandByObject(App.selected)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换选中模型
|
||||
*/
|
||||
objectSelected(){
|
||||
if(!this.isOpen) return;
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始剖切
|
||||
*/
|
||||
open() {
|
||||
this.initSectionBox();
|
||||
this.addMouseListener();
|
||||
this.isOpen = true;
|
||||
|
||||
useAddSignal("objectSelected", objectSelectedFn);
|
||||
useDispatchSignal("sceneGraphChanged");
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭剖切
|
||||
*/
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.removeMouseListener();
|
||||
this.clearSectionBox();
|
||||
|
||||
useRemoveSignal("objectSelected", objectSelectedFn);
|
||||
useDispatchSignal("sceneGraphChanged")
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置剖切
|
||||
*/
|
||||
reset() {
|
||||
this.close();
|
||||
this.open();
|
||||
|
||||
useDispatchSignal("sceneGraphChanged")
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if(this.isOpen){
|
||||
this.close();
|
||||
}
|
||||
|
||||
objectSelectedFn = undefined;
|
||||
}
|
||||
|
||||
// --------------- 剖切盒 --------------------
|
||||
|
||||
protected boxMin: Vector3 = new Vector3(); // 剖切盒最小点
|
||||
protected boxMax: Vector3 = new Vector3(); // 剖切盒最大点
|
||||
protected group: Group = new Group(); // 包含section的所有对象
|
||||
protected planes: Array<Plane> = []; // 切面
|
||||
protected vertices = [
|
||||
new Vector3(), new Vector3(), new Vector3(), new Vector3(), // 顶部有4个顶点
|
||||
new Vector3(), new Vector3(), new Vector3(), new Vector3() // 底部有4个顶点
|
||||
];
|
||||
|
||||
protected faces: Array<BoxFace> = [];
|
||||
protected lines: Array<BoxLine> = [];
|
||||
|
||||
/**
|
||||
* 初始化剖切盒
|
||||
*/
|
||||
protected initSectionBox() {
|
||||
this.setSectionBox();
|
||||
|
||||
// boxMin 与 boxMax 应增加内边距以免贴合
|
||||
this.boxMin = (this.sectionBox as Box3).min.sub(new Vector3(0.05, 0.05, 0.05));
|
||||
this.boxMax = (this.sectionBox as Box3).max.add(new Vector3(0.05, 0.05, 0.05));
|
||||
this.group = new Group();
|
||||
this.group.name = "clippedEdgesBox";
|
||||
this.group.ignore = true;
|
||||
this.initPlanes();
|
||||
this.initOrUpdateVertices();
|
||||
this.initOrUpdateFaces();
|
||||
this.initOrUpdateLines();
|
||||
this.viewer.scene.add(this.group);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化剖切盒的六面
|
||||
*/
|
||||
protected initPlanes() {
|
||||
this.planes = [];
|
||||
this.planes.push(
|
||||
new Plane(new Vector3(0, -1, 0), this.boxMax.y), // up
|
||||
new Plane(new Vector3(0, 1, 0), -this.boxMin.y), // down
|
||||
new Plane(new Vector3(1, 0, 0), -this.boxMin.x), // left
|
||||
new Plane(new Vector3(-1, 0, 0), this.boxMax.x), // right
|
||||
new Plane(new Vector3(0, 0, -1), this.boxMax.z), // front
|
||||
new Plane(new Vector3(0, 0, 1), -this.boxMin.z) // back
|
||||
);
|
||||
|
||||
const setChildClippingPlanes = (child) => {
|
||||
if (["Mesh", "LineSegments"].includes(child.type)) {
|
||||
child.material.clippingPlanes = this.planes;
|
||||
child.material.clipIntersection = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.lastSelected) {
|
||||
this.viewer.scene.traverseVisible(c => {
|
||||
setChildClippingPlanes(c)
|
||||
})
|
||||
} else {
|
||||
if (isGroup(this.lastSelected)) {
|
||||
App.traverseMeshToArr(this.lastSelected).forEach(child => {
|
||||
setChildClippingPlanes(child)
|
||||
})
|
||||
} else {
|
||||
setChildClippingPlanes(this.lastSelected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected updatePlanes() {
|
||||
this.planes[0].constant = this.boxMax.y;
|
||||
this.planes[1].constant = -this.boxMin.y;
|
||||
this.planes[2].constant = -this.boxMin.x;
|
||||
this.planes[3].constant = this.boxMax.x;
|
||||
this.planes[4].constant = this.boxMax.z;
|
||||
this.planes[5].constant = -this.boxMin.z;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化或更新剖切盒的8个顶点
|
||||
*/
|
||||
protected initOrUpdateVertices() {
|
||||
this.vertices[0].set(this.boxMin.x, this.boxMax.y, this.boxMin.z); // 顶部的四个顶点
|
||||
this.vertices[1].set(this.boxMax.x, this.boxMax.y, this.boxMin.z);
|
||||
this.vertices[2].set(this.boxMax.x, this.boxMax.y, this.boxMax.z);
|
||||
this.vertices[3].set(this.boxMin.x, this.boxMax.y, this.boxMax.z);
|
||||
this.vertices[4].set(this.boxMin.x, this.boxMin.y, this.boxMin.z); // 底部的四个顶点
|
||||
this.vertices[5].set(this.boxMax.x, this.boxMin.y, this.boxMin.z);
|
||||
this.vertices[6].set(this.boxMax.x, this.boxMin.y, this.boxMax.z);
|
||||
this.vertices[7].set(this.boxMin.x, this.boxMin.y, this.boxMax.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化或更新剖切盒的6个面
|
||||
*/
|
||||
protected initOrUpdateFaces() {
|
||||
const v = this.vertices;
|
||||
if (!this.faces || this.faces.length === 0) {
|
||||
this.faces = [];
|
||||
this.faces.push(
|
||||
new BoxFace("yUp", [v[0], v[1], v[2], v[3]]), // up
|
||||
new BoxFace("yDown", [v[4], v[7], v[6], v[5]]), // down
|
||||
new BoxFace("xLeft", [v[0], v[3], v[7], v[4]]), // left
|
||||
new BoxFace("xRight", [v[1], v[5], v[6], v[2]]), // right
|
||||
new BoxFace("zFront", [v[2], v[6], v[7], v[3]]), // front
|
||||
new BoxFace("zBack", [v[0], v[4], v[5], v[1]]) // back
|
||||
);
|
||||
this.group.add(...this.faces);
|
||||
this.faces.forEach(face => {
|
||||
this.group.add(face.backFace);
|
||||
});
|
||||
} else {
|
||||
const f = this.faces;
|
||||
f[0].setFromPoints([v[0], v[1], v[2], v[3]]);
|
||||
f[1].setFromPoints([v[4], v[7], v[6], v[5]]);
|
||||
f[2].setFromPoints([v[0], v[3], v[7], v[4]]);
|
||||
f[3].setFromPoints([v[1], v[5], v[6], v[2]]);
|
||||
f[4].setFromPoints([v[2], v[6], v[7], v[3]]);
|
||||
f[5].setFromPoints([v[0], v[4], v[5], v[1]]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化或更新剖切盒的12条边
|
||||
*/
|
||||
protected initOrUpdateLines() {
|
||||
const v = this.vertices;
|
||||
if (!this.lines || this.lines.length === 0) {
|
||||
const f = this.faces;
|
||||
if (!f) {
|
||||
throw Error("需要先初始化面!");
|
||||
}
|
||||
this.lines = [];
|
||||
this.lines.push(
|
||||
new BoxLine([v[0], v[1]], [f[0], f[5]]),
|
||||
new BoxLine([v[1], v[2]], [f[0], f[3]]),
|
||||
new BoxLine([v[2], v[3]], [f[0], f[4]]),
|
||||
new BoxLine([v[3], v[0]], [f[0], f[2]]),
|
||||
new BoxLine([v[4], v[5]], [f[1], f[5]]),
|
||||
new BoxLine([v[5], v[6]], [f[1], f[3]]),
|
||||
new BoxLine([v[6], v[7]], [f[1], f[4]]),
|
||||
new BoxLine([v[7], v[4]], [f[1], f[2]]),
|
||||
new BoxLine([v[0], v[4]], [f[2], f[5]]),
|
||||
new BoxLine([v[1], v[5]], [f[3], f[5]]),
|
||||
new BoxLine([v[2], v[6]], [f[3], f[4]]),
|
||||
new BoxLine([v[3], v[7]], [f[2], f[4]])
|
||||
);
|
||||
this.group.add(...this.lines);
|
||||
} else {
|
||||
let i = 0;
|
||||
this.lines[i++].setFromPoints([v[0], v[1]]);
|
||||
this.lines[i++].setFromPoints([v[1], v[2]]);
|
||||
this.lines[i++].setFromPoints([v[2], v[3]]);
|
||||
this.lines[i++].setFromPoints([v[3], v[0]]);
|
||||
this.lines[i++].setFromPoints([v[4], v[5]]);
|
||||
this.lines[i++].setFromPoints([v[5], v[6]]);
|
||||
this.lines[i++].setFromPoints([v[6], v[7]]);
|
||||
this.lines[i++].setFromPoints([v[7], v[4]]);
|
||||
this.lines[i++].setFromPoints([v[0], v[4]]);
|
||||
this.lines[i++].setFromPoints([v[1], v[5]]);
|
||||
this.lines[i++].setFromPoints([v[2], v[6]]);
|
||||
this.lines[i++].setFromPoints([v[3], v[7]]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除剖切盒
|
||||
*/
|
||||
protected clearSectionBox() {
|
||||
this.viewer.scene.remove(this.group);
|
||||
this.domElement.style.cursor = "";
|
||||
this.faces = [];
|
||||
this.lines = [];
|
||||
|
||||
const setChildClippingPlanes = (child) => {
|
||||
if (["Mesh", "LineSegments"].includes(child.type)) {
|
||||
child.material.clippingPlanes = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.lastSelected) {
|
||||
this.viewer.scene.traverseVisible(c => {
|
||||
setChildClippingPlanes(c);
|
||||
})
|
||||
} else {
|
||||
if (isGroup(this.lastSelected)) {
|
||||
App.traverseMeshToArr(this.lastSelected).forEach(child => {
|
||||
setChildClippingPlanes(child)
|
||||
})
|
||||
} else {
|
||||
setChildClippingPlanes(this.lastSelected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------- 指针事件 -----------------------
|
||||
|
||||
protected raycaster: Raycaster = new Raycaster();
|
||||
protected mousePosition: Vector2 = new Vector2();
|
||||
// 鼠标悬停的面激活
|
||||
protected activeFace: BoxFace | null = null;
|
||||
|
||||
private addMouseListener() {
|
||||
this.domElement.addEventListener("pointermove", this.onMouseMove);
|
||||
this.domElement.addEventListener("pointerdown", this.onMouseDown);
|
||||
}
|
||||
|
||||
private removeMouseListener() {
|
||||
this.domElement.removeEventListener("pointermove", this.onMouseMove);
|
||||
this.domElement.removeEventListener("pointerdown", this.onMouseDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换鼠标坐标,并更新光线投射
|
||||
*/
|
||||
protected updateMouseAndRay(event: MouseEvent) {
|
||||
this.mousePosition.setX((event.offsetX / this.domElement.offsetWidth) * 2 - 1);
|
||||
this.mousePosition.setY(-(event.offsetY / this.domElement.offsetHeight) * 2 + 1);
|
||||
this.raycaster.setFromCamera(this.mousePosition, this.viewer.camera);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标移动事件,正确高亮相应的面/线
|
||||
*/
|
||||
protected onMouseMove = (event: MouseEvent) => {
|
||||
this.updateMouseAndRay(event);
|
||||
const intersects = this.raycaster.intersectObjects(this.faces); // 鼠标和面相交
|
||||
if (intersects.length) {
|
||||
this.domElement.style.cursor = "pointer";
|
||||
const face = intersects[0].object as BoxFace;
|
||||
if (face !== this.activeFace) {
|
||||
if (this.activeFace) {
|
||||
this.activeFace.setActive(false);
|
||||
}
|
||||
face.setActive(true);
|
||||
this.activeFace = face;
|
||||
}
|
||||
} else {
|
||||
if (this.activeFace) {
|
||||
this.activeFace.setActive(false);
|
||||
this.activeFace = null;
|
||||
this.domElement.style.cursor = "auto";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理鼠标按下事件,开始使用左键拖动一个面
|
||||
*/
|
||||
protected onMouseDown = (event: MouseEvent) => {
|
||||
const isLeftButton = event.button === 0;
|
||||
|
||||
if (this.activeFace && isLeftButton) {
|
||||
this.updateMouseAndRay(event);
|
||||
const intersects = this.raycaster.intersectObjects(this.faces);
|
||||
if (intersects.length) {
|
||||
const face = intersects[0].object as BoxFace;
|
||||
const axis = face.axis;
|
||||
const point = intersects[0].point;
|
||||
this.drag.start(axis, point);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖动对象,用于处理面裁剪操作
|
||||
*/
|
||||
protected drag = {
|
||||
axis: "", // 要拖动的6轴之一
|
||||
point: new Vector3(), // 记录拖动点的位置
|
||||
ground: new Mesh(new PlaneGeometry(100000, 100000), new MeshBasicMaterial({
|
||||
colorWrite: false,
|
||||
depthWrite: false
|
||||
})),
|
||||
start: (axis: string, point: Vector3) => {
|
||||
this.drag.axis = axis;
|
||||
this.drag.point = point;
|
||||
this.drag.initGround();
|
||||
this.controls.enabled = false;
|
||||
this.domElement.style.cursor = "move";
|
||||
|
||||
this.domElement.removeEventListener("pointermove", this.onMouseMove);
|
||||
this.domElement.addEventListener("pointermove", this.drag.mousemove);
|
||||
this.domElement.addEventListener("pointerup", this.drag.mouseup);
|
||||
},
|
||||
end: () => {
|
||||
this.viewer.scene.remove(this.drag.ground);
|
||||
this.controls.enabled = true;
|
||||
this.domElement.removeEventListener("pointermove", this.drag.mousemove);
|
||||
this.domElement.removeEventListener("pointerup", this.drag.mouseup);
|
||||
this.domElement.addEventListener("pointermove", this.onMouseMove);
|
||||
},
|
||||
mousemove: (event: MouseEvent) => {
|
||||
this.updateMouseAndRay(event);
|
||||
const intersects = this.raycaster.intersectObject(this.drag.ground); // 鼠标与拖动地面的相交情况
|
||||
if (intersects.length) {
|
||||
this.drag.updateSectionBox(intersects[0].point);
|
||||
}
|
||||
},
|
||||
mouseup: () => {
|
||||
this.drag.end();
|
||||
},
|
||||
// 拖动时初始化参考平面,可以是XY, YZ, ZX平面
|
||||
initGround: () => {
|
||||
const normals: any = {
|
||||
xLeft: new Vector3(-1, 0, 0),
|
||||
xRight: new Vector3(1, 0, 0),
|
||||
yDown: new Vector3(0, -1, 0),
|
||||
yUp: new Vector3(0, 1, 0),
|
||||
zBack: new Vector3(0, 0, -1),
|
||||
zFront: new Vector3(0, 0, 1)
|
||||
};
|
||||
if (["xLeft", "xRight"].includes(this.drag.axis)) {
|
||||
this.drag.point.setX(0);
|
||||
} else if (["yDown", "yUp"].includes(this.drag.axis)) {
|
||||
this.drag.point.setY(0);
|
||||
} else if (["zBack", "zFront"].includes(this.drag.axis)) {
|
||||
this.drag.point.setZ(0);
|
||||
}
|
||||
this.drag.ground.position.copy(this.drag.point);
|
||||
const newNormal = this.viewer.camera.position.clone()
|
||||
.sub(this.viewer.camera.position.clone().projectOnVector(normals[this.drag.axis]))
|
||||
.add(this.drag.point); // 得到平面的法线
|
||||
this.drag.ground.lookAt(newNormal);
|
||||
this.viewer.scene.add(this.drag.ground);
|
||||
},
|
||||
// 更新裁剪盒子的位置
|
||||
updateSectionBox: (point: Vector3) => {
|
||||
const minSize = ClippedEdgesBox.MIN_WIDTH; // 截面框的最小尺寸
|
||||
switch (this.drag.axis) {
|
||||
case "yUp": // up
|
||||
this.boxMax.setY(Math.max(this.boxMin.y + minSize, point.y));
|
||||
break;
|
||||
case "yDown": // down
|
||||
this.boxMin.setY(Math.min(this.boxMax.y - minSize, point.y));
|
||||
break;
|
||||
case "xLeft": // left
|
||||
this.boxMin.setX(Math.min(this.boxMax.x - minSize, point.x));
|
||||
break;
|
||||
case "xRight": // right
|
||||
this.boxMax.setX(Math.max(this.boxMin.x + minSize, point.x));
|
||||
break;
|
||||
case "zFront": // front
|
||||
this.boxMax.setZ(Math.max(this.boxMin.z + minSize, point.z));
|
||||
break;
|
||||
case "zBack": // back
|
||||
this.boxMin.setZ(Math.min(this.boxMax.z - minSize, point.z));
|
||||
break;
|
||||
}
|
||||
|
||||
// 更新剖切盒的平面、顶点、面和线
|
||||
this.updatePlanes();
|
||||
this.initOrUpdateVertices();
|
||||
this.initOrUpdateFaces();
|
||||
this.initOrUpdateLines();
|
||||
|
||||
useDispatchSignal("sceneGraphChanged");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 剖切盒的BoxLine
|
||||
*/
|
||||
class BoxLine extends LineSegments {
|
||||
private normalMaterial = new LineBasicMaterial({color: 0x2ee3dc}); // 0x2ee3dc,线的正常颜色(原颜色:0xe1f2fb)
|
||||
private activeMaterial = new LineBasicMaterial({color: 0x00fdec}); // 0x00fdec,线的激活颜色(原始颜色:0x00ffff)
|
||||
|
||||
/**
|
||||
* @param vertices 一条直线上的两点
|
||||
* @param faces 相对于一条线的两个面
|
||||
*/
|
||||
constructor(vertices: Array<Vector3>, faces: Array<BoxFace>) {
|
||||
super();
|
||||
faces.forEach(face => face.lines.push(this)); // 保存面与线之间的关系
|
||||
this.geometry = new BufferGeometry();
|
||||
this.geometry.setFromPoints(vertices);
|
||||
this.material = this.normalMaterial;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 geometry
|
||||
*/
|
||||
setFromPoints(vertices: Vector3[]) {
|
||||
this.geometry.setFromPoints(vertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为活动或非活动状态
|
||||
* @param isActive
|
||||
*/
|
||||
setActive(isActive: boolean) {
|
||||
this.material = isActive ? this.activeMaterial : this.normalMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 剖切盒的BoxFace
|
||||
*/
|
||||
class BoxFace extends Mesh {
|
||||
axis: string;
|
||||
lines: Array<BoxLine> = []; // 4条线相对于一个面
|
||||
backFace: Mesh; // 背面:face的背面,用于显示
|
||||
|
||||
/**
|
||||
* @param axis 面的轴
|
||||
* @param vertices 面的四个点
|
||||
*/
|
||||
constructor(axis: string, vertices: Array<Vector3>) {
|
||||
super();
|
||||
this.axis = axis;
|
||||
this.lines = [];
|
||||
this.geometry = new BufferGeometry();
|
||||
this.geometry.setFromPoints(vertices);
|
||||
this.geometry.setIndex([0, 3, 2, 0, 2, 1]);
|
||||
this.geometry.computeVertexNormals();
|
||||
this.material = new MeshBasicMaterial({colorWrite: false, depthWrite: false});
|
||||
const backMaterial = new MeshBasicMaterial({color: 0x2ee3dc, transparent: true, opacity: 0.3, side: BackSide});
|
||||
this.backFace = new Mesh(this.geometry, backMaterial);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新geometry
|
||||
*/
|
||||
setFromPoints(vertices: Vector3[]) {
|
||||
this.geometry.setFromPoints(vertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为活动或非活动状态
|
||||
* @param isActive
|
||||
*/
|
||||
setActive(isActive: boolean) {
|
||||
this.lines.forEach(line => {
|
||||
line.setActive(isActive)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {ClippedEdgesBox};
|
||||
@@ -0,0 +1,260 @@
|
||||
import {saveArrayBuffer, saveString, getAnimations, getAnimationClips} from '@/utils';
|
||||
import App from "@/core/app/App";
|
||||
|
||||
class Export {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/*********************************导出物体*******************************************/
|
||||
//导出为JSON
|
||||
exportObjectToJSON() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const json = App.selected.toJSON();
|
||||
let output:string;
|
||||
try {
|
||||
output = JSON.stringify(json, null, '\t');
|
||||
output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1');
|
||||
} catch (e) {
|
||||
output = JSON.stringify(json);
|
||||
}
|
||||
|
||||
saveString(output, 'Astral3DModel.json');
|
||||
}
|
||||
|
||||
// 导出为glb
|
||||
async exportObjectToGlb() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const animations = getAnimationClips(App.selected);
|
||||
|
||||
const { GLTFExporter } = await import('three/examples/jsm/exporters/GLTFExporter.js');
|
||||
|
||||
const exporter = new GLTFExporter();
|
||||
|
||||
exporter.parse(
|
||||
App.selected,
|
||||
function (result) {
|
||||
saveArrayBuffer(result, 'Astral3DModel.glb');
|
||||
},
|
||||
(err) => {
|
||||
App.log.info(`导出物体为glb错误:${err.message}`)
|
||||
},
|
||||
{ binary: true, animations: animations }
|
||||
);
|
||||
}
|
||||
|
||||
//导出为gltf
|
||||
async exportObjectToGltf() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const animations = getAnimations();
|
||||
|
||||
const { GLTFExporter } = await import('three/examples/jsm/exporters/GLTFExporter.js');
|
||||
const exporter = new GLTFExporter();
|
||||
|
||||
exporter.parse(
|
||||
App.selected,
|
||||
function (result) {
|
||||
saveString(JSON.stringify(result, null, 2), 'Astral3DModel.gltf');
|
||||
},
|
||||
() => { },
|
||||
{ animations: animations }
|
||||
);
|
||||
}
|
||||
|
||||
//导出为obj
|
||||
async exportObjectToObj() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const { OBJExporter } = await import('three/examples/jsm/exporters/OBJExporter.js');
|
||||
|
||||
const exporter = new OBJExporter();
|
||||
|
||||
saveString(exporter.parse(App.selected), 'Astral3DModel.obj');
|
||||
}
|
||||
|
||||
//导出为ply
|
||||
async exportObjectToPly() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const { PLYExporter } = await import('three/examples/jsm/exporters/PLYExporter.js');
|
||||
|
||||
const exporter = new PLYExporter();
|
||||
|
||||
exporter.parse(
|
||||
App.selected,
|
||||
function (result) {
|
||||
saveArrayBuffer(result, 'Astral3DModel.ply');
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// 导出为ply二进制
|
||||
async exportObjectToPlyBinary() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const { PLYExporter } = await import('three/examples/jsm/exporters/PLYExporter.js');
|
||||
|
||||
const exporter = new PLYExporter();
|
||||
|
||||
exporter.parse(
|
||||
App.selected,
|
||||
function (result) {
|
||||
saveArrayBuffer(result, 'Astral3DModel-binary.ply');
|
||||
},
|
||||
{ binary: true }
|
||||
);
|
||||
}
|
||||
|
||||
//导出为STL
|
||||
async exportObjectToStl() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const { STLExporter } = await import('three/examples/jsm/exporters/STLExporter.js');
|
||||
|
||||
const exporter = new STLExporter();
|
||||
|
||||
saveString(exporter.parse(App.selected), 'Astral3DModel.stl');
|
||||
}
|
||||
|
||||
//导出为STL(二进制)
|
||||
async exportObjectToStlBinary() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const { STLExporter } = await import('three/examples/jsm/exporters/STLExporter.js');
|
||||
|
||||
const exporter = new STLExporter();
|
||||
|
||||
saveArrayBuffer(exporter.parse(App.selected, { binary: true }), 'Astral3DModel-binary.stl');
|
||||
}
|
||||
|
||||
//导出为USDZ
|
||||
async exportObjectToUSDZ() {
|
||||
if(!App.selected) return;
|
||||
|
||||
const { USDZExporter } = await import('three/examples/jsm/exporters/USDZExporter.js');
|
||||
|
||||
const exporter = new USDZExporter();
|
||||
saveArrayBuffer(await exporter.parseAsync(App.selected, {}), 'Astral3DModel.usdz');
|
||||
}
|
||||
|
||||
/*********************************导出场景*******************************************/
|
||||
//导出为JSON
|
||||
exportSceneToJSON() {
|
||||
const json = App.getSceneWithoutIgnore().toJSON();
|
||||
let output:string;
|
||||
try {
|
||||
output = JSON.stringify(json, null, '\t');
|
||||
output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1');
|
||||
} catch (e) {
|
||||
output = JSON.stringify(json);
|
||||
}
|
||||
|
||||
saveString(output, 'Astral3DScene.json');
|
||||
}
|
||||
|
||||
// 导出为glb
|
||||
async exportSceneToGlb(){
|
||||
const animations = getAnimationClips();
|
||||
|
||||
const { GLTFExporter } = await import('three/examples/jsm/exporters/GLTFExporter.js');
|
||||
|
||||
const exporter = new GLTFExporter();
|
||||
|
||||
exporter.parse(
|
||||
App.getSceneWithoutIgnore(),
|
||||
function (result) {
|
||||
saveArrayBuffer(result, 'Astral3DScene.glb');
|
||||
},
|
||||
() => {
|
||||
},
|
||||
{ binary: true, animations: animations }
|
||||
);
|
||||
}
|
||||
|
||||
//导出为gltf
|
||||
async exportSceneToGltf() {
|
||||
const animations = getAnimations();
|
||||
|
||||
const {GLTFExporter} = await import('three/examples/jsm/exporters/GLTFExporter.js');
|
||||
const exporter = new GLTFExporter();
|
||||
|
||||
exporter.parse(
|
||||
App.getSceneWithoutIgnore(),
|
||||
function (result) {
|
||||
saveString(JSON.stringify(result, null, 2), 'Astral3DScene.gltf');
|
||||
},
|
||||
() => {},
|
||||
{animations: animations}
|
||||
);
|
||||
}
|
||||
|
||||
//导出为obj
|
||||
async exportSceneToObj() {
|
||||
const { OBJExporter } = await import('three/examples/jsm/exporters/OBJExporter.js');
|
||||
|
||||
const exporter = new OBJExporter();
|
||||
|
||||
saveString(exporter.parse(App.getSceneWithoutIgnore()), 'Astral3DScene.obj');
|
||||
}
|
||||
|
||||
//导出为ply
|
||||
async exportSceneToPly() {
|
||||
const {PLYExporter} = await import('three/examples/jsm/exporters/PLYExporter.js');
|
||||
|
||||
const exporter = new PLYExporter();
|
||||
|
||||
exporter.parse(
|
||||
App.getSceneWithoutIgnore(),
|
||||
function (result) {
|
||||
saveArrayBuffer(result, 'Astral3DScene.ply');
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// 导出为ply二进制
|
||||
async exportSceneToPlyBinary() {
|
||||
const {PLYExporter} = await import('three/examples/jsm/exporters/PLYExporter.js');
|
||||
|
||||
const exporter = new PLYExporter();
|
||||
|
||||
exporter.parse(
|
||||
App.getSceneWithoutIgnore(),
|
||||
function (result) {
|
||||
saveArrayBuffer(result, 'Astral3DScene-binary.ply');
|
||||
},
|
||||
{binary: true}
|
||||
);
|
||||
}
|
||||
|
||||
//导出为STL
|
||||
async exportSceneToStl() {
|
||||
const {STLExporter} = await import('three/examples/jsm/exporters/STLExporter.js');
|
||||
|
||||
const exporter = new STLExporter();
|
||||
|
||||
saveString(exporter.parse(App.getSceneWithoutIgnore()), 'Astral3DScene.stl');
|
||||
}
|
||||
|
||||
//导出为STL(二进制)
|
||||
async exportSceneToStlBinary() {
|
||||
const {STLExporter} = await import('three/examples/jsm/exporters/STLExporter.js');
|
||||
|
||||
const exporter = new STLExporter();
|
||||
|
||||
saveArrayBuffer(exporter.parse(App.getSceneWithoutIgnore(), { binary: true }), 'Astral3DScene-binary.stl');
|
||||
}
|
||||
|
||||
//导出为USDZ
|
||||
async exportSceneToUSDZ() {
|
||||
const {USDZExporter} = await import('three/examples/jsm/exporters/USDZExporter.js');
|
||||
|
||||
const exporter = new USDZExporter();
|
||||
saveArrayBuffer(await exporter.parseAsync(App.getSceneWithoutIgnore(), {}), 'Astral3DScene.usdz');
|
||||
}
|
||||
}
|
||||
|
||||
export {Export};
|
||||
@@ -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};
|
||||
@@ -0,0 +1,123 @@
|
||||
import * as THREE from "three";
|
||||
import {MapControls} from "three/examples/jsm/controls/MapControls.js";
|
||||
|
||||
interface IMiniMapOptions {
|
||||
domElement?: HTMLElement;
|
||||
}
|
||||
|
||||
let onPointerDownFn;
|
||||
|
||||
|
||||
export class MiniMap {
|
||||
private viewport;
|
||||
private mapControl: MapControls;
|
||||
private domElement: HTMLElement;
|
||||
private miniMapRenderer: THREE.WebGLRenderer;
|
||||
private miniMapCamera: THREE.PerspectiveCamera;
|
||||
|
||||
raycaster = new THREE.Raycaster()
|
||||
|
||||
constructor(viewport,options:IMiniMapOptions = {}) {
|
||||
this.viewport = viewport;
|
||||
|
||||
this.domElement = options.domElement || this.createDomElement();
|
||||
viewport.container.appendChild(this.domElement);
|
||||
|
||||
const {renderer, camera} = this.init();
|
||||
this.miniMapRenderer = renderer;
|
||||
this.miniMapCamera = camera;
|
||||
|
||||
this.mapControl = this.initControls();
|
||||
|
||||
this.initEvent();
|
||||
|
||||
this.miniMapRenderer.setAnimationLoop(this.miniMapAnimation.bind(this));
|
||||
}
|
||||
|
||||
createDomElement(){
|
||||
const domElement = document.createElement("div");
|
||||
domElement.setAttribute("id", "es-3d-mini-map");
|
||||
domElement.style.position = "absolute";
|
||||
domElement.style.top = "10px";
|
||||
domElement.style.right = "10px";
|
||||
domElement.style.width = "250px";
|
||||
domElement.style.height = "250px";
|
||||
domElement.style.boxShadow = "0px 0px 5px #000";
|
||||
|
||||
return domElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为小地图准备专门的 渲染器 + 摄像机
|
||||
*/
|
||||
init(){
|
||||
const miniMapRenderer = new THREE.WebGLRenderer({antialias:true});
|
||||
miniMapRenderer.setSize(this.domElement.clientWidth, this.domElement.clientHeight);
|
||||
this.domElement.appendChild(miniMapRenderer.domElement);
|
||||
miniMapRenderer.setClearColor(0xffffff);
|
||||
|
||||
const miniMapCamera = new THREE.PerspectiveCamera(
|
||||
45,
|
||||
this.domElement.clientWidth / this.domElement.clientHeight,
|
||||
0.1,
|
||||
1000
|
||||
)
|
||||
miniMapCamera.position.set(0, 50, 0)
|
||||
miniMapCamera.lookAt(0,0,0);
|
||||
|
||||
return {
|
||||
renderer: miniMapRenderer,
|
||||
camera: miniMapCamera
|
||||
}
|
||||
}
|
||||
|
||||
initControls() {
|
||||
const controls = new MapControls(this.miniMapCamera, this.miniMapRenderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.enableZoom = false;
|
||||
|
||||
return controls;
|
||||
}
|
||||
|
||||
initEvent(){
|
||||
onPointerDownFn = this.onPointerDown.bind(this);
|
||||
this.domElement.addEventListener("pointerdown", onPointerDownFn);
|
||||
}
|
||||
|
||||
onPointerDown(e: PointerEvent){
|
||||
const mousePosition = new THREE.Vector2()
|
||||
|
||||
let x = e.offsetX
|
||||
let y = e.offsetY
|
||||
mousePosition.x = ((x / this.domElement.clientWidth) * 2) - 1;
|
||||
mousePosition.y = -((y / this.domElement.clientHeight) * 2) + 1;
|
||||
|
||||
this.raycaster.setFromCamera(mousePosition, this.miniMapCamera);
|
||||
let intersections = this.raycaster.intersectObject(this.viewport.scene, true);
|
||||
|
||||
if(intersections[0]){
|
||||
const point = intersections[0].point;
|
||||
|
||||
// this.miniMapCamera.position.set(point.x, 8, point.z);
|
||||
this.viewport.modules.controls.target.set(point.x, 0, point.z);
|
||||
}
|
||||
}
|
||||
|
||||
miniMapAnimation(){
|
||||
this.miniMapRenderer.render(this.viewport.scene, this.miniMapCamera)
|
||||
}
|
||||
|
||||
update(){
|
||||
this.mapControl.update();
|
||||
}
|
||||
|
||||
dispose(){
|
||||
this.mapControl.dispose();
|
||||
this.miniMapRenderer.dispose();
|
||||
this.domElement.removeChild(this.miniMapRenderer.domElement);
|
||||
|
||||
this.domElement.removeEventListener("pointerdown", onPointerDownFn);
|
||||
onPointerDownFn = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import {MathUtils, OrthographicCamera, PerspectiveCamera, Scene, WebGLRenderer,Object3D} from "three";
|
||||
import Viewer from "@/core/viewer/Viewer";
|
||||
|
||||
interface IMiniMapOptions {
|
||||
mapSize: number, // 决定了摄像机看到的内容大小, mapSize * mapSize 大小内容
|
||||
mapRenderSize: number, // 决定了小地图2D平面的大小
|
||||
followTarget:Object3D, // 小地图画面主要跟随对象
|
||||
isShow: boolean // 是否显示小地图
|
||||
}
|
||||
|
||||
class MiniMap {
|
||||
_miniMapCamera: OrthographicCamera | PerspectiveCamera | null = null;
|
||||
_miniMapRenderer: WebGLRenderer | null = null;
|
||||
_followTarget: Object3D;
|
||||
|
||||
private scene: Scene;
|
||||
private sceneHelpers: Scene;
|
||||
private mapRenderSize: number;
|
||||
// @ts-ignore
|
||||
public dom: HTMLDivElement;
|
||||
|
||||
isShow: boolean = false;
|
||||
|
||||
constructor(viewer:Viewer, options:IMiniMapOptions) {
|
||||
this.scene = viewer.scene;
|
||||
this.sceneHelpers = viewer.sceneHelpers;
|
||||
this.mapRenderSize = options.mapRenderSize;
|
||||
this._followTarget = options.followTarget;
|
||||
this.isShow = options.isShow;
|
||||
|
||||
if (!this.scene) {
|
||||
throw new Error("scene不能为空");
|
||||
}
|
||||
if (!this._followTarget) {
|
||||
throw new Error("target不能为空,表示小地图画面主要跟随对象");
|
||||
}
|
||||
|
||||
this.dom = this.createDomElement();
|
||||
viewer.container.appendChild(this.dom);
|
||||
|
||||
// 初始化小地图相机
|
||||
this._miniMapCamera = new OrthographicCamera(
|
||||
-options.mapSize / 2,
|
||||
options.mapSize / 2,
|
||||
options.mapSize / 2,
|
||||
-options.mapSize / 2,
|
||||
1, 100 * 1000
|
||||
);
|
||||
|
||||
// 更新地图相机位置和朝向
|
||||
this.updateCamera();
|
||||
}
|
||||
|
||||
createDomElement() {
|
||||
// 初始化小地图渲染器
|
||||
const mapRenderer = new WebGLRenderer({alpha: true});
|
||||
mapRenderer.setSize(this.mapRenderSize, this.mapRenderSize);
|
||||
// mapRenderer.setClearColor(0x7d684f);
|
||||
mapRenderer.shadowMap.enabled = false;
|
||||
mapRenderer.autoClear = false;
|
||||
this._miniMapRenderer = mapRenderer;
|
||||
|
||||
const pDiv = document.createElement("div");
|
||||
pDiv.id = "es-3d-mini-map";
|
||||
pDiv.style.position = "absolute";
|
||||
pDiv.style.right = "10px";
|
||||
pDiv.style.top = "10px";
|
||||
pDiv.style.zIndex = "1001";
|
||||
pDiv.style.border = "1px solid #FFF";
|
||||
pDiv.style.background = "rgba(0, 0, 0, 0.40)";
|
||||
pDiv.style.width = this.mapRenderSize - 100 + "px";
|
||||
pDiv.style.height = this.mapRenderSize - 100 + "px";
|
||||
pDiv.style.overflow = "hidden";
|
||||
pDiv.style.display = this.isShow ? "block" : "none";
|
||||
|
||||
mapRenderer.domElement.style.transform = `rotateZ(0deg)`;
|
||||
mapRenderer.domElement.style.width = this.mapRenderSize + "px";
|
||||
mapRenderer.domElement.style.height = this.mapRenderSize + "px";
|
||||
mapRenderer.domElement.style.position = "absolute";
|
||||
mapRenderer.domElement.style.left = "-50px";
|
||||
mapRenderer.domElement.style.top = "-50px";
|
||||
|
||||
pDiv.appendChild(mapRenderer.domElement);
|
||||
|
||||
return pDiv;
|
||||
}
|
||||
|
||||
open(){
|
||||
this.dom.style.display = "block";
|
||||
this.isShow = true;
|
||||
}
|
||||
|
||||
close(){
|
||||
this.dom.style.display = "none";
|
||||
this.isShow = false;
|
||||
}
|
||||
|
||||
updateCamera() {
|
||||
// 更新小地图css旋转角度,与玩家同步
|
||||
let targetRotateY = MathUtils.radToDeg(this._followTarget.rotation.y - Math.PI);
|
||||
(this._miniMapRenderer as WebGLRenderer).domElement.style.transform = `rotateZ(${targetRotateY}deg)`;
|
||||
|
||||
// 更新地图相机位置和朝向
|
||||
let targetPos = this._followTarget.position;
|
||||
(this._miniMapCamera as OrthographicCamera).position.set(
|
||||
targetPos.x,
|
||||
targetPos.y + 10,
|
||||
targetPos.z
|
||||
);
|
||||
(this._miniMapCamera as OrthographicCamera).lookAt(targetPos.x, 2, targetPos.z);
|
||||
}
|
||||
|
||||
update() {
|
||||
// 更新地图相机位置和朝向
|
||||
this.updateCamera();
|
||||
|
||||
const renderer = this._miniMapRenderer as WebGLRenderer;
|
||||
|
||||
renderer.autoClear = false;
|
||||
|
||||
// 渲染小地图
|
||||
renderer.render(this.scene, this._miniMapCamera as OrthographicCamera);
|
||||
|
||||
renderer.render(this.sceneHelpers, this._miniMapCamera as OrthographicCamera);
|
||||
|
||||
renderer.autoClear = true;
|
||||
}
|
||||
}
|
||||
|
||||
export {MiniMap}
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 模型爆炸展开
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import {useDispatchSignal} from "@/hooks";
|
||||
|
||||
interface IModelExplodeData {
|
||||
// 爆炸方向
|
||||
worldDir: THREE.Vector3;
|
||||
// 爆炸距离:mesh中心点到爆炸中心的距离
|
||||
worldDistance:THREE.Vector3;
|
||||
// 原始坐标
|
||||
originPosition:THREE.Vector3;
|
||||
// mesh中心
|
||||
meshCenter:THREE.Vector3;
|
||||
// 爆炸中心
|
||||
explodeCenter:THREE.Vector3;
|
||||
}
|
||||
|
||||
class ModelExplode{
|
||||
// 模型爆炸的展开数据
|
||||
meshExplodeData = new Map<string,Map<string,IModelExplodeData>>();
|
||||
|
||||
// 已执行模型爆炸未还原的模型
|
||||
unrestoredModel:THREE.Object3D[] = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算模型爆炸的展开数据
|
||||
*/
|
||||
computedExplodeData(model:THREE.Object3D):void{
|
||||
if(!model) return;
|
||||
|
||||
// 计算模型中心
|
||||
const modelBox = new THREE.Box3();
|
||||
modelBox.setFromObject(model);
|
||||
const explodeCenter = this.getWorldCenterPosition(modelBox);
|
||||
|
||||
const meshBox = new THREE.Box3();
|
||||
|
||||
const dataMap:Map<string,IModelExplodeData> = new Map();
|
||||
model.traverse((child) => {
|
||||
if(!child || !child.isMesh || child.isLine || child.isSprite) return;
|
||||
|
||||
meshBox.setFromObject(child);
|
||||
const meshCenter = this.getWorldCenterPosition(meshBox);
|
||||
const worldDistance = new THREE.Vector3().subVectors(meshCenter,explodeCenter)
|
||||
const meshExplodeData:IModelExplodeData = {
|
||||
worldDir: worldDistance.clone().normalize(),
|
||||
worldDistance:worldDistance,
|
||||
originPosition:child.getWorldPosition(new THREE.Vector3()),
|
||||
meshCenter:meshCenter.clone(),
|
||||
explodeCenter:explodeCenter.clone(),
|
||||
}
|
||||
|
||||
dataMap.set(child.uuid,meshExplodeData);
|
||||
})
|
||||
|
||||
this.meshExplodeData.set(model.uuid,dataMap);
|
||||
}
|
||||
|
||||
getWorldCenterPosition(box:THREE.Box3,scalar = 0.5){
|
||||
return new THREE.Vector3().addVectors(box.max,box.min).multiplyScalar(scalar);
|
||||
}
|
||||
|
||||
explodeModel(model:THREE.Object3D,scalar:number = 0.5){
|
||||
if(!this.meshExplodeData.has(model.uuid)){
|
||||
this.computedExplodeData(model);
|
||||
}
|
||||
|
||||
const dataMap = this.meshExplodeData.get(model.uuid);
|
||||
if(!dataMap) return;
|
||||
|
||||
model.traverse((child) => {
|
||||
if(!dataMap.has(child.uuid)) return;
|
||||
|
||||
const data = dataMap.get(child.uuid);
|
||||
if(!data) return;
|
||||
|
||||
const distance = data.worldDir.clone().multiplyScalar(data.worldDistance.length() * scalar);
|
||||
const offset = new THREE.Vector3().subVectors(data.meshCenter,data.originPosition);
|
||||
const center = data.explodeCenter;
|
||||
const newPosition = new THREE.Vector3().copy(center).add(distance).sub(offset);
|
||||
const localPosition = child.parent?.worldToLocal(newPosition);
|
||||
|
||||
if(localPosition){
|
||||
child.position.copy(localPosition);
|
||||
}
|
||||
})
|
||||
|
||||
useDispatchSignal("sceneGraphChanged");
|
||||
|
||||
this.unrestoredModel.push(model);
|
||||
}
|
||||
|
||||
// 还原
|
||||
restore(){
|
||||
this.unrestoredModel.forEach(model=>{
|
||||
const dataMap = this.meshExplodeData.get(model.uuid);
|
||||
if(!dataMap) return;
|
||||
|
||||
model.traverse((child) => {
|
||||
if(!dataMap.has(child.uuid)) return;
|
||||
|
||||
const data = dataMap.get(child.uuid);
|
||||
if(!data) return;
|
||||
|
||||
const _originPosition = child.parent?.worldToLocal(data.originPosition);
|
||||
|
||||
if(_originPosition){
|
||||
child.position.copy(_originPosition);
|
||||
}
|
||||
})
|
||||
|
||||
useDispatchSignal("sceneGraphChanged");
|
||||
})
|
||||
|
||||
this.unrestoredModel = [];
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear(){
|
||||
this.meshExplodeData.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export {ModelExplode};
|
||||
@@ -0,0 +1,709 @@
|
||||
/**
|
||||
* @author ErSan
|
||||
* @email mlt131220@163.com
|
||||
* @date 2024/08/28
|
||||
* @description 漫游类,使用BVH检测碰撞,人物模型必须包含动画:Enter,Idle, Walking, WalkingBackward,Jumping
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import CameraControls from 'camera-controls';
|
||||
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
||||
import {RoundedBoxGeometry} from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
|
||||
import {GenerateMeshBVHWorker} from '@/workers/bvh/GenerateMeshBVHWorker.js';
|
||||
import {useDispatchSignal} from "@/hooks";
|
||||
import {getMeshByInstancedMesh} from "@/utils";
|
||||
import {RoamingStatus} from "./RoamingStatus";
|
||||
import Loader from "@/core/loader/Loader";
|
||||
import App from "@/core/app/App";
|
||||
import Viewer from "@/core/viewer/Viewer";
|
||||
import MergeGeometriesWorker from "@/workers/mergeGeometries.worker.ts?worker&url";
|
||||
|
||||
let keyDownFn, keyUpFn;
|
||||
|
||||
class Roaming {
|
||||
private viewer: Viewer;
|
||||
private controls: CameraControls;
|
||||
|
||||
group: THREE.Group;
|
||||
private collider: THREE.Mesh | undefined; // 碰撞器
|
||||
private player: THREE.Mesh | undefined; // 碰撞胶囊体
|
||||
person: THREE.Group | undefined; // 人物
|
||||
|
||||
private playerIsOnGround = true;
|
||||
private playerVelocity = new THREE.Vector3();
|
||||
private gravity = -25; // 重力
|
||||
private playerSpeed = 3; // 人物移动速度
|
||||
playerInitPos = new THREE.Vector3(0, 0, 0); // 人物初始位置
|
||||
private firstPerson = true; // 是否第一人称
|
||||
|
||||
// 按键监听
|
||||
private fwdPressed = false;
|
||||
private bkdPressed = false;
|
||||
private lftPressed = false;
|
||||
private rgtPressed = false;
|
||||
|
||||
private upVector = new THREE.Vector3(0, 1, 0);
|
||||
private tempVector = new THREE.Vector3();
|
||||
private tempVector2 = new THREE.Vector3();
|
||||
private tempBox = new THREE.Box3();
|
||||
private tempMat = new THREE.Matrix4();
|
||||
private tempSegment = new THREE.Line3();
|
||||
|
||||
public isRoaming = false; // 是否在漫游
|
||||
mergeWorker: Worker;
|
||||
private generateMeshBVHWorker: GenerateMeshBVHWorker;
|
||||
private personStatus: RoamingStatus | null = null;
|
||||
|
||||
constructor(viewer: Viewer) {
|
||||
this.viewer = viewer;
|
||||
this.controls = viewer.modules.controls;
|
||||
|
||||
keyDownFn = this.keyDown.bind(this);
|
||||
window.addEventListener('keydown', keyDownFn);
|
||||
|
||||
keyUpFn = this.keyUp.bind(this);
|
||||
window.addEventListener('keyup', keyUpFn);
|
||||
|
||||
this.group = new THREE.Group();
|
||||
this.group.name = "es-3d-roaming-group";
|
||||
this.group.visible = false;
|
||||
this.group.ignore = true;
|
||||
|
||||
this.mergeWorker = new Worker(MergeGeometriesWorker, {type: 'module'});
|
||||
this.generateMeshBVHWorker = new GenerateMeshBVHWorker();
|
||||
|
||||
this.addPlayer();
|
||||
}
|
||||
|
||||
keyDown(e: KeyboardEvent) {
|
||||
if (!this.isRoaming || e.repeat) return;
|
||||
|
||||
switch (e.code) {
|
||||
case 'KeyW':
|
||||
this.fwdPressed = true;
|
||||
this.personStatus?.setStatus("w", true);
|
||||
break;
|
||||
case 'KeyS':
|
||||
this.bkdPressed = true;
|
||||
this.personStatus?.setStatus("s", true);
|
||||
break;
|
||||
case 'KeyD':
|
||||
this.rgtPressed = true;
|
||||
this.personStatus?.setStatus("d", true);
|
||||
break;
|
||||
case 'KeyA':
|
||||
this.lftPressed = true;
|
||||
this.personStatus?.setStatus("a", true);
|
||||
break;
|
||||
case 'Space':
|
||||
if (this.personStatus?.keyDownStatus.space) return;
|
||||
|
||||
if (this.playerIsOnGround) {
|
||||
// 跳跃动画有30FPS准备动作
|
||||
setTimeout(() => {
|
||||
this.playerVelocity.y = 10.0;
|
||||
this.playerIsOnGround = false;
|
||||
}, (30 / App.FPS) * 1000)
|
||||
}
|
||||
this.personStatus?.setStatus("space", true);
|
||||
break;
|
||||
case "ShiftLeft":
|
||||
case "ShiftRight":
|
||||
if (this.personStatus?.isWalkingForward) {
|
||||
this.playerSpeed = 6;
|
||||
this.personStatus?.setStatus("shift", true);
|
||||
}
|
||||
break;
|
||||
case 'KeyV': // 切换第一/第三人称视角
|
||||
this.firstPerson = !this.firstPerson;
|
||||
|
||||
if (this.firstPerson) { //人称切换
|
||||
// 第一人称
|
||||
this.controls.maxPolarAngle = Math.PI / 2;
|
||||
this.controls.minDistance = 0.8;
|
||||
this.controls.maxDistance = 0.8;
|
||||
this.controls.distance = 0.8;
|
||||
} else {
|
||||
this.controls.maxPolarAngle = Math.PI / 2;
|
||||
this.controls.minDistance = 6;
|
||||
this.controls.maxDistance = 6;
|
||||
this.controls.distance = 6;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
keyUp(e: KeyboardEvent) {
|
||||
if (!this.isRoaming || e.repeat) return;
|
||||
|
||||
switch (e.code) {
|
||||
case 'KeyW':
|
||||
this.personStatus?.setStatus("w", false);
|
||||
this.fwdPressed = false;
|
||||
break;
|
||||
case 'KeyS':
|
||||
this.personStatus?.setStatus("s", false);
|
||||
this.bkdPressed = false;
|
||||
break;
|
||||
case 'KeyD':
|
||||
this.personStatus?.setStatus("d", false);
|
||||
this.rgtPressed = false;
|
||||
break;
|
||||
case 'KeyA':
|
||||
this.personStatus?.setStatus("a", false);
|
||||
this.lftPressed = false;
|
||||
break;
|
||||
case "ShiftLeft":
|
||||
case "ShiftRight":
|
||||
this.playerSpeed = 3;
|
||||
this.personStatus?.setStatus("shift", false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加漫游所需人物模型
|
||||
addPlayer(){
|
||||
// 几何圆柱体 用于碰撞检测
|
||||
const cylinder = new THREE.Mesh(
|
||||
new RoundedBoxGeometry(0.5, 1.7, 0.5, 10, 0.5),
|
||||
new THREE.MeshStandardMaterial()
|
||||
)
|
||||
cylinder.geometry.translate(0, -0.6, 0);
|
||||
// @ts-ignore
|
||||
cylinder.capsuleInfo = {
|
||||
radius: 0.4,
|
||||
segment: new THREE.Line3(new THREE.Vector3(), new THREE.Vector3(0, -1.0, 0.0))
|
||||
}
|
||||
cylinder.name = 'es-3d-roaming-cylinder';
|
||||
cylinder.visible = false;
|
||||
|
||||
this.player = cylinder;
|
||||
this.group.add(cylinder);
|
||||
|
||||
this.reloadPerson();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置漫游人物模型
|
||||
*/
|
||||
async reloadPerson() {
|
||||
// 加载人物模型glb
|
||||
const loader = await Loader.createGLTFLoader();
|
||||
|
||||
const done = (blob) => {
|
||||
// 加载人物模型Blob
|
||||
loader.loadAsync(URL.createObjectURL(blob)).then(result => {
|
||||
const person = result.scene as THREE.Group;
|
||||
person.name = "es-3d-roaming-player";
|
||||
|
||||
if(this.person){
|
||||
person.matrix.copy(this.person.matrix);
|
||||
person.matrixWorld.copy(this.person.matrixWorld);
|
||||
|
||||
this.person.removeFromParent();
|
||||
}
|
||||
|
||||
this.person = person;
|
||||
this.group.add(person);
|
||||
|
||||
// 漫游人物动画状态机
|
||||
if(this.personStatus){
|
||||
this.personStatus.dispose();
|
||||
}
|
||||
this.personStatus = new RoamingStatus(person, result.animations);
|
||||
});
|
||||
}
|
||||
|
||||
// 从本地DB读取人物模型
|
||||
const playerConfig= App.config.getKey("roamingCharacter")
|
||||
App.storage.getModel(`player-${playerConfig}`).then((file: Blob | unknown) => {
|
||||
if (!file) {
|
||||
const playerGlbUrl = new URL(`${import.meta.env.BASE_URL}resource/model/${playerConfig}.glb`, import.meta.url).href;
|
||||
// 加载默认人物模型
|
||||
fetch(playerGlbUrl).then(res => res.blob()).then(blob => {
|
||||
App.storage.setModel(`player-${playerConfig}`, blob)
|
||||
done(blob);
|
||||
})
|
||||
} else {
|
||||
done(file);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生成碰撞器环境
|
||||
generateColliderEnvironment() {
|
||||
let mergedGeometry:any;
|
||||
|
||||
// TODO:20251003 - environment组好像没有存在的意义?运行两个月无误后删除
|
||||
//const environment = new THREE.Group();
|
||||
//environment.name = "astral-3d-roaming-collider-environment";
|
||||
|
||||
const generateBVH = () => {
|
||||
return new Promise(resolve => {
|
||||
this.generateMeshBVHWorker.generate(mergedGeometry).then(bvh => {
|
||||
// @ts-ignore
|
||||
mergedGeometry.boundsTree = bvh;
|
||||
|
||||
this.collider = new THREE.Mesh(mergedGeometry);
|
||||
// @ts-ignore
|
||||
this.collider.material.wireframe = false;
|
||||
this.collider.name = "astral-3d-roaming-collider";
|
||||
this.collider.visible = false;
|
||||
|
||||
// @ts-ignore
|
||||
this.group.add(this.collider);
|
||||
|
||||
resolve("");
|
||||
|
||||
this.generateMeshBVHWorker.dispose();
|
||||
});
|
||||
|
||||
//environment.visible = false;
|
||||
//this.group.add(environment);
|
||||
this.viewer.scene.add(this.group);
|
||||
})
|
||||
}
|
||||
|
||||
const generateMergedGeometry = () => {
|
||||
return new Promise((resolve,reject) => {
|
||||
const cloneGeom = (me) => {
|
||||
// 检查对应属性是否存在
|
||||
if (!me.geometry.attributes || !me.geometry.attributes.position || me.geometry.attributes.position.isInterleavedBufferAttribute) return;
|
||||
|
||||
let geom = me.geometry.clone();
|
||||
geom.applyMatrix4(me.matrixWorld);
|
||||
|
||||
// 合并仅保留position即可
|
||||
geom.attributes = {
|
||||
position: geom.toNonIndexed().attributes.position, // 取消position索引
|
||||
}
|
||||
|
||||
// 手动纠正有些模型没有顶点索引的问题
|
||||
if (geom.index) geom.index = null;
|
||||
|
||||
this.mergeWorker.postMessage({
|
||||
type: "push",
|
||||
// geometry: geom
|
||||
// 合并在容差范围内的具有相似属性的顶点
|
||||
geometry: BufferGeometryUtils.mergeVertices(geom)
|
||||
})
|
||||
}
|
||||
|
||||
this.viewer.scene.traverseByCondition(c => {
|
||||
// requestIdleCallback(()=>{
|
||||
// 只合并网格
|
||||
if (c.geometry) {
|
||||
// @ts-ignore
|
||||
if (!c.isInstancedMesh) {
|
||||
cloneGeom(c);
|
||||
} else {
|
||||
const meshes = getMeshByInstancedMesh(c as THREE.InstancedMesh);
|
||||
meshes.forEach((m: THREE.Mesh) => {
|
||||
cloneGeom(m);
|
||||
});
|
||||
}
|
||||
}
|
||||
// })
|
||||
}, (c) => !c.ignore && !c.isTilesGroup && !c.isTiles && c.visible)
|
||||
|
||||
// requestIdleCallback(()=>{
|
||||
this.mergeWorker.postMessage({
|
||||
type: "merge"
|
||||
})
|
||||
// })
|
||||
|
||||
this.mergeWorker.onmessage = (event) => {
|
||||
if(event.data.type === "error") {
|
||||
// 有可能是纯3DTiles场景
|
||||
if(this.viewer.modules.tilesManage.tilesMap.size === 0){
|
||||
reject(event.data.message);
|
||||
}else{
|
||||
resolve("");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.data.geometry) return;
|
||||
|
||||
mergedGeometry = event.data.geometry;
|
||||
mergedGeometry.__proto__ = THREE.BufferGeometry.prototype;
|
||||
mergedGeometry.index && (mergedGeometry.index.__proto__ = THREE.BufferAttribute.prototype);
|
||||
mergedGeometry.attributes.position.__proto__ = THREE.BufferAttribute.prototype;
|
||||
mergedGeometry.attributes.normal && (mergedGeometry.attributes.normal.__proto__ = THREE.BufferAttribute.prototype);
|
||||
|
||||
// 删除uv属性
|
||||
if (mergedGeometry.attributes.uv) {
|
||||
mergedGeometry.deleteAttribute("uv");
|
||||
}
|
||||
|
||||
// const newMesh = new THREE.Mesh(mergedGeometry, new THREE.MeshBasicMaterial());
|
||||
//const newMesh = new THREE.Mesh(BufferGeometryUtils.mergeVertices(mergedGeometry), new THREE.MeshBasicMaterial());
|
||||
//newMesh.visible = false;
|
||||
|
||||
//environment.add(newMesh);
|
||||
|
||||
generateBVH().then(() => {
|
||||
resolve("");
|
||||
});
|
||||
|
||||
// 关闭 worker
|
||||
this.mergeWorker.terminate();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return generateMergedGeometry();
|
||||
}
|
||||
|
||||
// 重置人物位置
|
||||
resetPlayer() {
|
||||
const player = this.player as THREE.Mesh;
|
||||
|
||||
this.playerVelocity.set(0, 0, 0);
|
||||
player.position.copy(this.playerInitPos);
|
||||
|
||||
// 播放模型进入动画
|
||||
this.personStatus?.init();
|
||||
|
||||
const _target = new THREE.Vector3();
|
||||
this.controls.getTarget(_target);
|
||||
this.viewer.camera.position.sub(_target);
|
||||
this.controls.setTarget(player.position.x, player.position.y + 2, player.position.z, false);
|
||||
this.controls.distance = this.firstPerson ? 0.8 : 6;
|
||||
this.viewer.camera.position.add(player.position);
|
||||
this.controls.update(0.016);
|
||||
}
|
||||
|
||||
// 进入漫游
|
||||
startRoaming() {
|
||||
if (this.isRoaming) return;
|
||||
|
||||
this.group.visible = true;
|
||||
|
||||
this.viewer.computedSceneBox3();
|
||||
|
||||
this.resetPlayer();
|
||||
|
||||
this.isRoaming = true;
|
||||
}
|
||||
|
||||
// 退出漫游
|
||||
exitRoaming(lastRoadCameraPos = new THREE.Vector3(1, 1, 1), lastRoadCameraTarget = new THREE.Vector3()) {
|
||||
this.group.visible = false;
|
||||
|
||||
lastRoadCameraPos && this.controls.setPosition(lastRoadCameraPos.x, lastRoadCameraPos.y, lastRoadCameraPos.z, true);
|
||||
lastRoadCameraTarget && this.controls.setTarget(lastRoadCameraTarget.x, lastRoadCameraTarget.y, lastRoadCameraTarget.z, true);
|
||||
|
||||
this.controls.maxPolarAngle = Math.PI;
|
||||
this.controls.minDistance = 0;
|
||||
this.controls.maxDistance = Infinity;
|
||||
|
||||
this.controls.update(0.016);
|
||||
this.isRoaming = false;
|
||||
|
||||
// 停用混合器上所有预定的动作
|
||||
this.personStatus?.stopAllAction();
|
||||
|
||||
useDispatchSignal("sceneGraphChanged");
|
||||
}
|
||||
|
||||
render(delta: number) {
|
||||
if (!delta) return;
|
||||
|
||||
const player = this.player as THREE.Object3D;
|
||||
|
||||
// =========================
|
||||
// 重力与竖直方向
|
||||
// =========================
|
||||
if (this.playerIsOnGround) {
|
||||
this.playerVelocity.y = delta * this.gravity;
|
||||
} else {
|
||||
this.playerVelocity.y += delta * this.gravity;
|
||||
}
|
||||
player.position.addScaledVector(this.playerVelocity, delta);
|
||||
|
||||
// =========================
|
||||
// 水平方向移动
|
||||
// =========================
|
||||
const angle = this.controls.azimuthAngle;
|
||||
if (this.fwdPressed) {
|
||||
this.tempVector.set(0, 0, -1).applyAxisAngle(this.upVector, angle);
|
||||
player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
|
||||
}
|
||||
if (this.bkdPressed) {
|
||||
this.tempVector.set(0, 0, 1).applyAxisAngle(this.upVector, angle);
|
||||
player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
|
||||
}
|
||||
if (this.lftPressed) {
|
||||
this.tempVector.set(-1, 0, 0).applyAxisAngle(this.upVector, angle);
|
||||
player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
|
||||
}
|
||||
if (this.rgtPressed) {
|
||||
this.tempVector.set(1, 0, 0).applyAxisAngle(this.upVector, angle);
|
||||
player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
|
||||
}
|
||||
|
||||
player.updateMatrixWorld();
|
||||
|
||||
// =========================
|
||||
// 碰撞检测
|
||||
// =========================
|
||||
// @ts-ignore
|
||||
const capsuleInfo = (player as any).capsuleInfo;
|
||||
const worldSegStart = capsuleInfo.segment.start.clone().applyMatrix4(player.matrixWorld);
|
||||
|
||||
// 收集所有 collider mesh
|
||||
const colliders: THREE.Mesh[] = [];
|
||||
if (this.viewer.modules.tilesManage.mergeMesh) {
|
||||
colliders.push(this.viewer.modules.tilesManage.mergeMesh);
|
||||
}
|
||||
if (this.collider) colliders.push(this.collider);
|
||||
|
||||
let chosenNewPositionWorld: THREE.Vector3 | null = null;
|
||||
let maxOffsetLen = -Infinity;
|
||||
|
||||
for (const mesh of colliders) {
|
||||
if (!mesh.geometry?.boundsTree) continue;
|
||||
|
||||
// 胶囊段:player.local → world → mesh.local
|
||||
this.tempMat.copy(mesh.matrixWorld).invert();
|
||||
this.tempSegment.copy(capsuleInfo.segment);
|
||||
this.tempSegment.start.applyMatrix4(player.matrixWorld).applyMatrix4(this.tempMat);
|
||||
this.tempSegment.end.applyMatrix4(player.matrixWorld).applyMatrix4(this.tempMat);
|
||||
|
||||
// AABB for shapecast
|
||||
this.tempBox.makeEmpty();
|
||||
this.tempBox.expandByPoint(this.tempSegment.start);
|
||||
this.tempBox.expandByPoint(this.tempSegment.end);
|
||||
this.tempBox.min.addScalar(-capsuleInfo.radius);
|
||||
this.tempBox.max.addScalar(capsuleInfo.radius);
|
||||
|
||||
// 执行 shapecast,会修改 this.tempSegment
|
||||
mesh.geometry.boundsTree.shapecast({
|
||||
intersectsBounds: box => box.intersectsBox(this.tempBox),
|
||||
intersectsTriangle: tri => {
|
||||
// 检查三角形是否与胶囊相交,如果相交则调整胶囊位置。
|
||||
const triPoint = this.tempVector;
|
||||
const capsulePoint = this.tempVector2;
|
||||
|
||||
const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
|
||||
if (distance < (this.player as THREE.Object3D).capsuleInfo.radius) {
|
||||
const depth = (this.player as THREE.Object3D).capsuleInfo.radius - distance;
|
||||
const direction = capsulePoint.sub(triPoint).normalize();
|
||||
|
||||
this.tempSegment.start.addScaledVector(direction, depth);
|
||||
this.tempSegment.end.addScaledVector(direction, depth);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 结果变回 world 空间
|
||||
const adjustedWorld = this.tempSegment.start.clone().applyMatrix4(mesh.matrixWorld);
|
||||
|
||||
const offsetLen = adjustedWorld.distanceTo(worldSegStart);
|
||||
if (offsetLen > maxOffsetLen) {
|
||||
maxOffsetLen = offsetLen;
|
||||
chosenNewPositionWorld = adjustedWorld;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用最终选择的位移
|
||||
if (chosenNewPositionWorld) {
|
||||
const deltaVector = this.tempVector2.subVectors(chosenNewPositionWorld, player.position);
|
||||
|
||||
this.playerIsOnGround = deltaVector.y > Math.abs(delta * this.playerVelocity.y * 0.25);
|
||||
|
||||
const offset = Math.max(0.0, deltaVector.length() - 1e-5);
|
||||
deltaVector.normalize().multiplyScalar(offset);
|
||||
|
||||
player.position.add(deltaVector);
|
||||
|
||||
if (!this.playerIsOnGround) {
|
||||
deltaVector.normalize();
|
||||
this.playerVelocity.addScaledVector(deltaVector, -deltaVector.dot(this.playerVelocity));
|
||||
} else {
|
||||
this.playerVelocity.set(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 相机调整
|
||||
// =========================
|
||||
const v = new THREE.Vector3(player.position.x, player.position.y + 0.2, player.position.z);
|
||||
const _target = new THREE.Vector3();
|
||||
this.controls.getTarget(_target);
|
||||
this.viewer.camera.position.sub(_target);
|
||||
this.controls.setTarget(v.x, v.y, v.z, false);
|
||||
this.controls.distance = this.firstPerson ? 0.8 : 6;
|
||||
this.viewer.camera.position.add(v);
|
||||
this.controls.polarAngle = Math.PI / 2;
|
||||
|
||||
// 人物模型位置跟随
|
||||
if (this.person) {
|
||||
this.person.position.set(player.position.x, player.position.y - 1.415, player.position.z);
|
||||
}
|
||||
|
||||
// 跌落检测
|
||||
if (this.viewer.sceneBox3 && (this.viewer.sceneBox3.min.y - player.position.y > 15)) {
|
||||
requestAnimationFrame(() => this.resetPlayer());
|
||||
}
|
||||
|
||||
// 动画状态更新
|
||||
this.personStatus?.update(delta);
|
||||
}
|
||||
|
||||
// render(delta: number) {
|
||||
// if (!delta) return;
|
||||
//
|
||||
// const player = this.player as THREE.Object3D;
|
||||
//
|
||||
// if (this.playerIsOnGround) {
|
||||
// this.playerVelocity.y = delta * this.gravity;
|
||||
// } else {
|
||||
// this.playerVelocity.y += delta * this.gravity;
|
||||
// }
|
||||
//
|
||||
// // 人物竖直方向移动(跳跃)
|
||||
// player.position.addScaledVector(this.playerVelocity, delta);
|
||||
//
|
||||
// /* 人物移动 */
|
||||
// const angle = this.controls.azimuthAngle;
|
||||
// if (this.fwdPressed) {
|
||||
// this.tempVector.set(0, 0, -1).applyAxisAngle(this.upVector, angle);
|
||||
// player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
|
||||
// }
|
||||
//
|
||||
// if (this.bkdPressed) {
|
||||
// this.tempVector.set(0, 0, 1).applyAxisAngle(this.upVector, angle);
|
||||
// player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
|
||||
// }
|
||||
//
|
||||
// if (this.lftPressed) {
|
||||
// this.tempVector.set(-1, 0, 0).applyAxisAngle(this.upVector, angle);
|
||||
// player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
|
||||
// }
|
||||
//
|
||||
// if (this.rgtPressed) {
|
||||
// this.tempVector.set(1, 0, 0).applyAxisAngle(this.upVector, angle);
|
||||
// player.position.addScaledVector(this.tempVector, this.playerSpeed * delta);
|
||||
// }
|
||||
//
|
||||
// player.updateMatrixWorld();
|
||||
//
|
||||
// // @ts-ignore 根据碰撞调整位置
|
||||
// const capsuleInfo = player.capsuleInfo;
|
||||
// this.tempBox.makeEmpty();
|
||||
// this.tempMat.copy((this.collider as THREE.Mesh).matrixWorld).invert();
|
||||
// this.tempSegment.copy(capsuleInfo.segment);
|
||||
//
|
||||
// // 获得胶囊在碰撞器的局部空间中的位置
|
||||
// this.tempSegment.start.applyMatrix4(player.matrixWorld).applyMatrix4(this.tempMat);
|
||||
// this.tempSegment.end.applyMatrix4(player.matrixWorld).applyMatrix4(this.tempMat);
|
||||
//
|
||||
// // 获取胶囊的轴对齐边界框
|
||||
// this.tempBox.expandByPoint(this.tempSegment.start);
|
||||
// this.tempBox.expandByPoint(this.tempSegment.end);
|
||||
// this.tempBox.min.addScalar(-capsuleInfo.radius);
|
||||
// this.tempBox.max.addScalar(capsuleInfo.radius);
|
||||
//
|
||||
// this.collider?.geometry.boundsTree?.shapecast({
|
||||
// intersectsBounds: box => box.intersectsBox(this.tempBox),
|
||||
// intersectsTriangle: tri => {
|
||||
// // 检查三角形是否与胶囊相交,如果相交则调整胶囊位置。
|
||||
// const triPoint = this.tempVector;
|
||||
// const capsulePoint = this.tempVector2;
|
||||
//
|
||||
// const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
|
||||
// if (distance < (this.player as THREE.Object3D).capsuleInfo.radius) {
|
||||
// const depth = (this.player as THREE.Object3D).capsuleInfo.radius - distance;
|
||||
// const direction = capsulePoint.sub(triPoint).normalize();
|
||||
//
|
||||
// this.tempSegment.start.addScaledVector(direction, depth);
|
||||
// this.tempSegment.end.addScaledVector(direction, depth);
|
||||
// }
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if(this.viewer.modules.tilesManage.tilesMap.size > 0){
|
||||
// this.viewer.modules.tilesManage.mergeMesh?.geometry.boundsTree?.shapecast({
|
||||
// intersectsBounds: box => box.intersectsBox(this.tempBox),
|
||||
// intersectsTriangle: tri => {
|
||||
// // 检查三角形是否与胶囊相交,如果相交则调整胶囊位置。
|
||||
// const triPoint = this.tempVector;
|
||||
// const capsulePoint = this.tempVector2;
|
||||
//
|
||||
// const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
|
||||
// if (distance < (this.player as THREE.Object3D).capsuleInfo.radius) {
|
||||
// const depth = (this.player as THREE.Object3D).capsuleInfo.radius - distance;
|
||||
// const direction = capsulePoint.sub(triPoint).normalize();
|
||||
//
|
||||
// this.tempSegment.start.addScaledVector(direction, depth);
|
||||
// this.tempSegment.end.addScaledVector(direction, depth);
|
||||
// }
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// // 在检查三角形碰撞并移动后,获得胶囊碰撞器在世界空间中的调整位置。假设capsule.info.segment.start是玩家模型的原点。
|
||||
// const newPosition = this.tempVector;
|
||||
// newPosition.copy(this.tempSegment.start).applyMatrix4((this.collider as THREE.Mesh).matrixWorld);
|
||||
//
|
||||
// // 检查碰撞器移动了多少
|
||||
// const deltaVector = this.tempVector2;
|
||||
// deltaVector.subVectors(newPosition, player.position);
|
||||
//
|
||||
// // 如果玩家主要是垂直调整,我们就会认为它是在地面上
|
||||
// this.playerIsOnGround = deltaVector.y > Math.abs(delta * this.playerVelocity.y * 0.25);
|
||||
//
|
||||
// const offset = Math.max(0.0, deltaVector.length() - 1e-5);
|
||||
// deltaVector.normalize().multiplyScalar(offset);
|
||||
//
|
||||
// // 调整玩家模型的位置;
|
||||
// player.position.add(deltaVector);
|
||||
// if (!this.playerIsOnGround) {
|
||||
// deltaVector.normalize();
|
||||
// this.playerVelocity.addScaledVector(deltaVector, -deltaVector.dot(this.playerVelocity));
|
||||
// } else {
|
||||
// this.playerVelocity.set(0, 0, 0);
|
||||
// }
|
||||
//
|
||||
// // 调整相机
|
||||
// const v = new THREE.Vector3(player.position.x, player.position.y + 0.2, player.position.z);
|
||||
// const _target = new THREE.Vector3();
|
||||
// this.controls.getTarget(_target);
|
||||
// this.viewer.camera.position.sub(_target);
|
||||
// this.controls.setTarget(v.x, v.y, v.z, false);
|
||||
// this.controls.distance = this.firstPerson ? 0.8 : 6;
|
||||
// this.viewer.camera.position.add(v);
|
||||
// this.controls.polarAngle = Math.PI / 2;
|
||||
//
|
||||
// if (this.person) {
|
||||
// const p = player.position.clone();
|
||||
// this.person.position.set(p.x, p.y - 1.415, p.z);
|
||||
// }
|
||||
//
|
||||
// //如果玩家跌得太低,将他们的位置重置到起点
|
||||
// if (this.viewer.sceneBox3 && (this.viewer.sceneBox3.min.y - player.position.y > 15)) {
|
||||
// this.resetPlayer();
|
||||
// }
|
||||
//
|
||||
// this.personStatus?.update(delta);
|
||||
// }
|
||||
|
||||
dispose() {
|
||||
window.removeEventListener('keydown', keyDownFn);
|
||||
window.removeEventListener('keyup', keyUpFn);
|
||||
|
||||
App.removeObject(this.group);
|
||||
|
||||
this.personStatus?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export {Roaming}
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @author ErSan
|
||||
* @email mlt131220@163.com
|
||||
* @date 2024/8/29 22:10
|
||||
* @description 漫游的人物动画状态机
|
||||
*/
|
||||
import * as THREE from "three";
|
||||
|
||||
export class RoamingStatus{
|
||||
// 键盘按下状态
|
||||
keyDownStatus = {
|
||||
w:false,
|
||||
s:false,
|
||||
a:false,
|
||||
d:false,
|
||||
shift:false,
|
||||
space:false
|
||||
}
|
||||
|
||||
fadeTime = 0.2;
|
||||
|
||||
person: THREE.Group; // 人物
|
||||
mixer: THREE.AnimationMixer; // 动画混合器
|
||||
private clipAction: { [s: string]: THREE.AnimationAction } = {}; // 动画 action
|
||||
|
||||
constructor(person:THREE.Group,clips:THREE.AnimationClip[]) {
|
||||
this.person = person;
|
||||
this.mixer = new THREE.AnimationMixer(this.person);
|
||||
clips.forEach(clip => {
|
||||
this.clipAction[clip.name] = (this.mixer as THREE.AnimationMixer).clipAction(clip);
|
||||
})
|
||||
|
||||
this.mixer.addEventListener('loop', (e) => {
|
||||
switch (e.action.getClip().name){
|
||||
case "Enter":
|
||||
this.fadeIn("Idle");
|
||||
setTimeout(() => {
|
||||
this.clipAction.Enter.stop();
|
||||
},200)
|
||||
break;
|
||||
case "Jumping":
|
||||
this.setStatus("space",false);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init(){
|
||||
this.clipAction.Idle.play();
|
||||
}
|
||||
|
||||
// 是否正在向前走
|
||||
get isWalkingForward(){
|
||||
return this.keyDownStatus.w || this.keyDownStatus.a || this.keyDownStatus.d;
|
||||
}
|
||||
|
||||
setStatus(key:string,value: boolean){
|
||||
this.keyDownStatus[key] = value;
|
||||
|
||||
switch (key){
|
||||
case "w":
|
||||
case "a":
|
||||
case "d":
|
||||
// 前进动画处理
|
||||
if(!this.keyDownStatus.w && !this.keyDownStatus.a && !this.keyDownStatus.d){
|
||||
this.fadeOut("Walking");
|
||||
|
||||
if(this.clipAction.Running.isRunning()){
|
||||
this.keyDownStatus.shift = false;
|
||||
this.fadeOut("Running")
|
||||
}
|
||||
}else{
|
||||
if(!this.clipAction.Walking.isRunning()){
|
||||
this.fadeIn("Walking");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "s":
|
||||
// 后退处理
|
||||
if(value){
|
||||
this.fadeIn("WalkingBackward");
|
||||
}else{
|
||||
this.fadeOut("WalkingBackward")
|
||||
}
|
||||
break;
|
||||
case "space":
|
||||
// 跳跃处理
|
||||
if(value){
|
||||
this.fadeIn("Jumping");
|
||||
|
||||
if(this.clipAction.Walking.isRunning()){
|
||||
this.fadeOut("Walking")
|
||||
}
|
||||
if(this.clipAction.Running.isRunning()){
|
||||
this.fadeOut("Running")
|
||||
}
|
||||
}else{
|
||||
if(this.keyDownStatus.shift){
|
||||
if(!this.clipAction.Running.isRunning()){
|
||||
this.fadeIn("Running")
|
||||
}
|
||||
}else if(this.isWalkingForward){
|
||||
if(!this.clipAction.Walking.isRunning()){
|
||||
this.fadeIn("Walking")
|
||||
}
|
||||
}
|
||||
|
||||
this.fadeOut("Jumping")
|
||||
}
|
||||
break;
|
||||
case "shift":
|
||||
if(value){
|
||||
this.fadeIn("Running");
|
||||
|
||||
this.fadeOut("Walking")
|
||||
}else{
|
||||
if(this.isWalkingForward){
|
||||
if(!this.clipAction.Walking.isRunning()){
|
||||
this.fadeIn("Walking");
|
||||
this.fadeOut("Running");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if(!Object.values(this.keyDownStatus).includes(true)){
|
||||
if(!this.clipAction.Idle.isRunning()){
|
||||
this.fadeIn("Idle");
|
||||
}
|
||||
}else{
|
||||
if(this.clipAction.Idle.isRunning()){
|
||||
this.fadeOut("Idle");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fadeIn(name: string) {
|
||||
// if(!this.clipAction[name].isScheduled()){
|
||||
// this.clipAction[name].reset();
|
||||
// this.clipAction[name].play();
|
||||
// }
|
||||
//
|
||||
// this.clipAction[name].fadeIn(this.fadeTime);
|
||||
|
||||
this.clipAction[name].play();
|
||||
}
|
||||
|
||||
fadeOut(name:string){
|
||||
// this.clipAction[name].fadeOut(this.fadeTime)
|
||||
// setTimeout(() => {
|
||||
// this.clipAction[name].stop();
|
||||
// },this.fadeTime * 100)
|
||||
|
||||
this.clipAction[name].stop();
|
||||
}
|
||||
|
||||
stopAllAction(){
|
||||
// 停用混合器上所有预定的动作
|
||||
this.mixer.stopAllAction();
|
||||
}
|
||||
|
||||
|
||||
update(delta:number){
|
||||
this.mixer.update(delta);
|
||||
|
||||
// THREE.AnimationMixer loop事件不一定有用,此处添加一个检测
|
||||
if(!this.clipAction.Jumping.isRunning() && this.keyDownStatus.space){
|
||||
//console.log("THREE.AnimationMixer loop事件未生效,手动设置")
|
||||
this.keyDownStatus.space = false;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(){
|
||||
this.stopAllAction();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export {Roaming} from "./Roaming";
|
||||
export {RoamingStatus} from "./RoamingStatus";
|
||||
export {MiniMap} from "./MiniMap";
|
||||
export {ClippedEdgesBox} from "./ClippedEdgesBox";
|
||||
export {Measure,MeasureMode} from "./Measure";
|
||||
export {Export} from "./Export";
|
||||
export {ModelExplode} from "./ModelExplode";
|
||||
Reference in New Issue
Block a user