feat(All):Initial

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