TkAstral3D/packages/sdk/lib/core/tools/ClippedEdgesBox.ts
2025-10-04 23:36:07 +08:00

559 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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};