559 lines
19 KiB
TypeScript
559 lines
19 KiB
TypeScript
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}; |