支持六面方向拖拽、反向、切面填充.
代码:
import * as THREE from 'three'
import { MouseHandler } from 'src/renderers/input/mouse'
import {mergeGeometries} from 'three/examples/jsm/utils/BufferGeometryUtils'
import {BaseHandle} from './base'
import {HANDLE_TYPES} from '../constant'
const DEFAULT_EMPTY_ARRAY:any=[]
function createPlaneStencilGroup(geometry:THREE.BufferGeometry, plane:THREE.Plane, renderOrder:number) {
const group = new THREE.Group();
const baseMat = new THREE.MeshBasicMaterial();
baseMat.color.setHex(0x0000ff)
baseMat.depthWrite = false;
baseMat.depthTest = false;
baseMat.colorWrite = false;
baseMat.stencilWrite = true;
baseMat.stencilFunc = THREE.AlwaysStencilFunc;
// back faces
const mat0 = baseMat.clone();
mat0.side = THREE.BackSide;
mat0.clippingPlanes = [plane];
mat0.stencilFail = THREE.IncrementWrapStencilOp;
mat0.stencilZFail = THREE.IncrementWrapStencilOp;
mat0.stencilZPass = THREE.IncrementWrapStencilOp;
const mesh0 = new THREE.Mesh(geometry, mat0);
mesh0.renderOrder = renderOrder;
group.add(mesh0);
// front faces
const mat1 = baseMat.clone();
mat1.side = THREE.FrontSide;
mat1.clippingPlanes = [plane];
mat1.stencilFail = THREE.DecrementWrapStencilOp;
mat1.stencilZFail = THREE.DecrementWrapStencilOp;
mat1.stencilZPass = THREE.DecrementWrapStencilOp;
const mesh1 = new THREE.Mesh(geometry, mat1);
mesh1.renderOrder = renderOrder;
group.add(mesh1);
return group;
}
class ClipFace extends THREE.Mesh{
materialActive:THREE.MeshBasicMaterial
materialInactive:THREE.MeshBasicMaterial
lines:ClipLine[]=[]
constructor(public vertices:{dirty:boolean,position:THREE.Vector3}[],public axes:string){
super()
this.materialInactive=new THREE.MeshBasicMaterial({
colorWrite:false,
depthWrite:false,
})
this.materialActive=new THREE.MeshBasicMaterial({
color:0x00aaff,
transparent:true,
opacity:0.3
})
this.geometry=new THREE.BufferGeometry()
this.geometry.setAttribute('position',new THREE.BufferAttribute(new Float32Array(4*3),3));
this.geometry.setIndex(new THREE.BufferAttribute(new Uint16Array([2,1,0,3,2,0]),1));
(this.geometry.attributes.position as THREE.Float32BufferAttribute).setUsage(THREE.DynamicDrawUsage);
this.setActive(false)
this.updateVertices(true)
}
updateVertices(forceUpdate=false){
let needsUpdate=false
this.vertices.forEach((p,i)=>{
if((forceUpdate||p.dirty)){
needsUpdate=true
const v=p.position;
this.geometry.attributes.position.setXYZ(i,v.x,v.y,v.z)
}
})
this.geometry.attributes.position.needsUpdate=needsUpdate
}
setActive(active:boolean){
this.material=active?this.materialActive:this.materialInactive
this.lines.forEach(line=>{
line.setActive(active)
})
}
}
class ClipLine extends THREE.LineSegments{
activeColor:number=0x00aaff
defaultColor:number=0x88aaff
constructor(public vertices:{dirty:boolean,position:THREE.Vector3}[]){
super()
this.material=new THREE.LineBasicMaterial({
color:this.defaultColor
})
this.geometry=new THREE.BufferGeometry()
this.geometry.setAttribute('position',new THREE.BufferAttribute(new Float32Array(2*3),3));
(this.geometry.attributes.position as THREE.Float32BufferAttribute).setUsage(THREE.DynamicDrawUsage);
this.updateVertices(true)
}
updateVertices(forceUpdate=false){
let needsUpdate=false
this.vertices.forEach((p,i)=>{
if((forceUpdate||p.dirty)){
needsUpdate=true
const v=p.position;
this.geometry.attributes.position.setXYZ(i,v.x,v.y,v.z)
}
})
this.geometry.attributes.position.needsUpdate=needsUpdate
}
setActive(active:boolean){
(this.material as THREE.LineBasicMaterial).color.setHex(active?this.activeColor:this.defaultColor)
}
}
interface ClipBoxOptions{
cap?:boolean
negative?:boolean
renderer:THREE.WebGLRenderer
scene:THREE.Scene
camera:()=>THREE.Camera
update:()=>void
onDragRange?:()=>void
onDragStart?:()=>void
onDragEnd?:()=>void
}
enum ClipBoxState{
NONE,// 未激活
CLIP,// 显示剖切框
CLIP_EFFECT, // 隐藏剖切框,显示剖切效果
EXIT // 退出
}
class ClipBox{
min=new THREE.Vector3()
max=new THREE.Vector3()
initMin=new THREE.Vector3()
initMax=new THREE.Vector3()
box3=new THREE.Box3()
target?:THREE.Object3D
group:THREE.Group=new THREE.Group()
planes?:THREE.Plane[]
planeObjects?:THREE.Mesh[]
vertices?:{dirty:boolean,position:THREE.Vector3}[]
lines?:ClipLine[]
faces?:ClipFace[]
boxGeometry!:THREE.BoxGeometry
basicMaterial?:THREE.MeshBasicMaterial
hoverMaterial?:THREE.MeshBasicMaterial
_clipScene=new THREE.Group()
mouseHandle!:MouseHandler
state:ClipBoxState=ClipBoxState.NONE
constructor(public options:ClipBoxOptions){
this.options={
cap:false,
negative:false,
...this.options
}
}
get clipScene(){
return this._clipScene
}
get renderer(){
return this.options.renderer
}
get camera(){
return this.options.camera()
}
initClipBox(){
this.clipScene.matrixWorldAutoUpdate=false;
this.clipScene.matrixAutoUpdate=false
this.clipScene.matrixWorldNeedsUpdate=false;
this.box3.setFromObject(this.target!).expandByScalar(1.2)
this.min.copy(this.box3.min)
this.initMin.copy(this.box3.min)
this.max.copy(this.box3.max)
this.initMax.copy(this.box3.max)
this.initPlanes()
this.initVertices()
this.initFaces()
this.initLines()
this.updatePlanes()
this.initEvents()
this.initCap()
this.updateTargetClipPlanes()
this.options.scene.add(this.clipScene)
}
// 还原
reset(){
this.min.copy(this.initMin)
this.max.copy(this.initMax)
this.update()
}
// 显示剖切框
showClip(){
if(this.state===ClipBoxState.CLIP){
return
}
if(this.state===ClipBoxState.NONE){
this.initClipBox()
}else if(this.state===ClipBoxState.EXIT){
this.updateTargetClipPlanes()
}
this.updateAllClipBox()
this.renderer.localClippingEnabled=true;
this.state=ClipBoxState.CLIP
this.options.scene.add(this.clipScene)
this.attachInteractive()
this.update()
}
// 隐藏剖切框
hideClip(){
if(this.state===ClipBoxState.CLIP){
this.state=ClipBoxState.CLIP_EFFECT
this.options.scene.remove(this.clipScene)
this.mouseHandle.detachEvents()
this.update()
}
}
attach(obj:THREE.Object3D){
this.target=obj
}
update(){
this.options.update()
}
detach(){
this.target=undefined
}
exit(){
if(this.state===ClipBoxState.EXIT){
return
}
this.min.copy(this.initMin)
this.max.copy(this.initMax)
this.renderer.localClippingEnabled=false
this.state=ClipBoxState.EXIT
this.options.scene.remove(this.clipScene)
this.mouseHandle.detachEvents()
this.resetTargetClipPlanes()
this.target=undefined
this.update()
}
initEvents(){
this.mouseHandle=new MouseHandler(this.renderer.domElement,document,()=>this.camera)
const plane=new THREE.Plane()
const cameraDirection=new THREE.Vector3()
const startPoint=new THREE.Vector3()
const deltaPoint=new THREE.Vector3()
const intersectionPoint=new THREE.Vector3()
this.mouseHandle.addEventListener('object-enter',e=>{
(e.intersections[0].object as ClipFace).setActive(true)
this.update()
})
this.mouseHandle.addEventListener('object-leave',e=>{
(e.intersections[0].object as ClipFace).setActive(false)
this.update()
})
this.mouseHandle.addEventListener('select',e=>{
this.camera.getWorldDirection(cameraDirection)
plane.setFromNormalAndCoplanarPoint(cameraDirection,e.target.selectedIntersections[0].point)
if(this.mouseHandle.raycaster.ray.intersectPlane(plane,intersectionPoint)){
startPoint.copy(intersectionPoint)
}
this.options.onDragStart?.()
e.target.pointerEvent?.stopImmediatePropagation()
})
this.mouseHandle.addEventListener('object-drag',e=>{
this.mouseHandle.updateRaycaster()
if(this.mouseHandle.raycaster.ray.intersectPlane(plane,intersectionPoint)){
const dragObject=e.target.selectedIntersections[0].object as ClipFace
deltaPoint.copy(intersectionPoint).sub(startPoint)
startPoint.copy(intersectionPoint)
if(dragObject.axes==='x+'){
this.max.x=Math.min(this.initMax.x,Math.max(this.initMin.x,this.max.x+deltaPoint.x))
}
if(dragObject.axes==='x-'){
this.min.x=Math.min(this.initMax.x,Math.max(this.initMin.x,this.min.x+deltaPoint.x))
}
if(dragObject.axes==='y+'){
this.max.y=Math.min(this.initMax.y,Math.max(this.initMin.y,this.max.y+deltaPoint.y))
}
if(dragObject.axes==='y-'){
this.min.y=Math.min(this.initMax.y,Math.max(this.initMin.y,this.min.y+deltaPoint.y))
}
if(dragObject.axes==='z+'){
this.max.z=Math.min(this.initMax.z,Math.max(this.initMin.z,this.max.z+deltaPoint.z))
}
if(dragObject.axes==='z-'){
this.min.z=Math.min(this.initMax.z,Math.max(this.initMin.z,this.min.z+deltaPoint.z))
}
this.updateClipBox(dragObject)
this.options.onDragRange?.()
this.update()
}
})
this.mouseHandle.addEventListener('pointerup',e=>{
this.options.onDragEnd?.()
})
}
attachInteractive(){
this.mouseHandle.hoverObjects=this.faces!.slice()
this.mouseHandle.selectObjects=this.faces!.slice();
this.mouseHandle.attachEvents()
}
initPlanes(){
this.planes=[
new THREE.Plane(new THREE.Vector3(1, 0, 0), 0),// 左
new THREE.Plane(new THREE.Vector3(-1, 0, 0), 0), // 右
new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),// 下
new THREE.Plane(new THREE.Vector3(0, -1, 0), 0),// 上
new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), // 后
new THREE.Plane(new THREE.Vector3(0, 0, -1), 0), // 前
]
}
initVertices(){
this.vertices=[
{dirty:false,position:new THREE.Vector3(this.min.x,this.min.y,this.min.z)},// left bottom
{dirty:false,position: new THREE.Vector3(this.min.x,this.max.y,this.min.z)},// left top
{dirty:false,position:new THREE.Vector3(this.max.x,this.max.y,this.min.z)}, // right top
{dirty:false,position:new THREE.Vector3(this.max.x,this.min.y,this.min.z)}, // right bottom
// positive
{dirty:false,position:new THREE.Vector3(this.min.x,this.min.y,this.max.z)},// left bottom
{dirty:false,position:new THREE.Vector3(this.min.x,this.max.y,this.max.z)},// left top
{dirty:false,position:new THREE.Vector3(this.max.x,this.max.y,this.max.z)}, // right top
{dirty:false,position:new THREE.Vector3(this.max.x,this.min.y,this.max.z)}, // right bottom
]
}
initFaces(){
this.faces=[
new ClipFace([7,6,2,3].map(i=>this.vertices![i]),'x+'),// x+
new ClipFace([0,1,5,4].map(i=>this.vertices![i]),'x-'), // x-
new ClipFace([5,1,2,6].map(i=>this.vertices![i]),'y+'),// y+
new ClipFace([7,3,0,4].map(i=>this.vertices![i]),'y-'),// y-
new ClipFace([3,2,1,0].map(i=>this.vertices![i]),'z-'), // z-
new ClipFace([4,5,6,7].map(i=>this.vertices![i]),'z+'), // z+
]
this.faces.forEach(face=>{
this.clipScene.add(face)
})
}
initLines(){
this.lines=[
new ClipLine([0,1].map(i=>this.vertices![i])),// positive left 0
new ClipLine([1,2].map(i=>this.vertices![i])),// positive top 1
new ClipLine([2,3].map(i=>this.vertices![i])),// positive right 2
new ClipLine([3,0].map(i=>this.vertices![i])),// positive bottom 3
new ClipLine([4,5].map(i=>this.vertices![i])),// negative left 4
new ClipLine([5,6].map(i=>this.vertices![i])),// negative top 5
new ClipLine([6,7].map(i=>this.vertices![i])),// negative right 6
new ClipLine([7,4].map(i=>this.vertices![i])),// negative bottom 7
new ClipLine([1,5].map(i=>this.vertices![i])),// left top 8
new ClipLine([0,4].map(i=>this.vertices![i])),// left bottom 9
new ClipLine([2,6].map(i=>this.vertices![i])),// right top 10
new ClipLine([3,7].map(i=>this.vertices![i])),// right bottom 11
]
this.faces![0].lines=[10,11,2,6].map(d=>this.lines![d])
this.faces![1].lines=[8,9,0,4].map(d=>this.lines![d])
this.faces![2].lines=[8,10,1,5].map(d=>this.lines![d])
this.faces![3].lines=[9,11,3,7].map(d=>this.lines![d])
this.faces![4].lines=[0,1,2,3].map(d=>this.lines![d])
this.faces![5].lines=[4,5,6,7].map(d=>this.lines![d])
this.lines.forEach(line=>{
this.clipScene.add(line)
})
}
capRemoves=[]
initCap(){
if(!this.options.cap&&this.planeObjects){
this.capRemoves.forEach(obj=>{
this.clipScene.remove(obj)
})
this.planeObjects=undefined
this.capRemoves=[]
}
if(this.planeObjects){
return
}
const planeObjects = this.planeObjects=[] as THREE.Mesh[];
const size=new THREE.Vector3()
this.box3.getSize(size)
// const center=this.box3.getCenter(new THREE.Vector3())
const max=Math.max(size.x,size.y,size.z)
const planeGeom = new THREE.PlaneGeometry(max,max);
let planes=this.planes!;
const geometries:THREE.BufferGeometry[]=[]
this.target!.updateMatrix()
this.target?.updateMatrixWorld(true)
this.target?.traverse((obj)=>{
if((obj as any).isMesh||obj.isLine){
const geometry=(obj as THREE.BufferGeometry).geometry.clone()
geometry.applyMatrix4(obj.matrixWorld)
geometries.push(geometry)
}
})
const geometry=mergeGeometries(Array.from(geometries))
const object = new THREE.Group();
object.name='stencilGroup'
this.clipScene.add( object );
this.capRemoves.push(object)
for ( let i = 0; i < planes.length; i ++ ) {
const poGroup = new THREE.Group();
const plane = planes[ i ];
const stencilGroup = createPlaneStencilGroup( geometry, plane, i + 1 );
const planeMat =
new THREE.MeshNormalMaterial( {
// color: 0xff0000,
// depthTest:false,
polygonOffset:true,
polygonOffsetFactor:1,
polygonOffsetUnits:0,
clippingPlanes: planes.filter( p => p !== plane ),
stencilWrite: true,
stencilRef: 0,
// side:THREE.DoubleSide,
stencilFunc: THREE.NotEqualStencilFunc,
stencilFail: THREE.ReplaceStencilOp,
stencilZFail: THREE.ReplaceStencilOp,
stencilZPass: THREE.ReplaceStencilOp,
} );
const po = new THREE.Mesh( planeGeom, planeMat );
po.onAfterRender = function ( renderer ) {
renderer.clearStencil();
};
po.renderOrder = i + 1.1;
object.add( stencilGroup );
poGroup.add( po );
planeObjects.push(po);
this.capRemoves.push(poGroup)
this.clipScene.add( poGroup );
}
}
resetTargetClipPlanes(){
this.target!.traverse((obj:any)=>{
if(obj.isMesh||obj.isLine){
if(Array.isArray(obj.material)){
obj.material.forEach((m:any)=>{
m.clipIntersection=false;
m.clippingPlanes=DEFAULT_EMPTY_ARRAY
})
}else{
obj.material.clipIntersection=false
obj.material.clippingPlanes=DEFAULT_EMPTY_ARRAY
}
}
})
}
updateTargetClipPlanes(){
this.target!.traverse((obj:any)=>{
if(obj.isMesh||obj.isLine){
if(Array.isArray(obj.material)){
obj.material.forEach((m:any)=>{
m.clipIntersection=this.options.negative
m.clippingPlanes=this.planes
})
}else{
obj.material.clipIntersection=this.options.negative
obj.material.clippingPlanes=this.planes
}
}
})
}
updatePlanes(){
const planes=this.planes!;
planes[0].normal.set(1, 0, 0)
planes[1].normal.set(-1, 0, 0)
planes[2].normal.set(0, 1, 0)
planes[3].normal.set(0, -1, 0)
planes[4].normal.set(0, 0, 1)
planes[5].normal.set(0, 0, -1)
planes[0].constant=-this.min.x;
planes[1].constant=this.max.x;
planes[2].constant=-this.min.y;
planes[3].constant=this.max.y;
planes[4].constant=-this.min.z;
planes[5].constant=this.max.z;
if(this.options.negative){
planes.forEach(plane=>{
plane.negate()
})
}
}
updateVertices(){
const vertices=this.vertices!;
vertices[0].position.set(this.min.x,this.min.y,this.min.z)
vertices[1].position.set(this.min.x,this.max.y,this.min.z)
vertices[2].position.set(this.max.x,this.max.y,this.min.z)
vertices[3].position.set(this.max.x,this.min.y,this.min.z)
vertices[4].position.set(this.min.x,this.min.y,this.max.z)
vertices[5].position.set(this.min.x,this.max.y,this.max.z)
vertices[6].position.set(this.max.x,this.max.y,this.max.z)
vertices[7].position.set(this.max.x,this.min.y,this.max.z)
}
updateClipBox(face: ClipFace){
this.updateVertices()
face.vertices.forEach(v=>{
v.dirty=true;
})
this.faces?.forEach(face=>{
face.updateVertices()
})
this.lines?.forEach(line=>{
line.updateVertices()
})
this.vertices?.forEach(v=>{
v.dirty=false;
})
this.updatePlanes()
this.updateClipCap()
}
updateAllClipBox(){
this.initCap()
this.updateVertices()
this.faces?.forEach(face=>{
face.vertices.forEach(v=>{
v.dirty=true;
})
})
this.faces?.forEach(face=>{
face.updateVertices()
})
this.lines?.forEach(line=>{
line.updateVertices()
})
this.vertices?.forEach(v=>{
v.dirty=false;
})
this.updatePlanes()
this.updateClipCap()
this.updateTargetClipPlanes()
this.update()
}
updateClipBoxFromAxes(axes:string){
this.updateVertices()
this.faces?.forEach(face=>{
if(face.axes.charAt(0)===axes.toLowerCase()){
face.vertices.forEach(v=>{
v.dirty=true;
})
}
})
this.faces?.forEach(face=>{
face.updateVertices()
})
this.lines?.forEach(line=>{
line.updateVertices()
})
this.vertices?.forEach(v=>{
v.dirty=false;
})
this.updatePlanes()
this.updateClipCap()
}
updateClipCap(){
if(!this.options.cap){
return
}
const planes=this.planes!;
const planeObjects=this.planeObjects!;
const center=this.initMin.clone().add(this.initMax).multiplyScalar(0.5)
const targetDist=[this.min.x-center.x,center.x-this.max.x,this.min.y-center.y,center.y-this.max.y,this.min.z-center.z,center.z-this.max.z]
const size=new THREE.Vector3()
this.box3.getSize(size)
const max=Math.max(size.x,size.y,size.z)
for ( let i = 0; i < planeObjects.length; i ++ ) {
const po = planeObjects[ i ];
const plane = planes[i];
// plane.coplanarPoint( po.position );
po.material.clipIntersection=this.options.negative
if(this.options.negative){
po.material.clippingPlanes=this.planes.map(p=>{
return p.clone().translate(p.normal.clone().negate());
})
}else{
po.material.clippingPlanes=this.planes?.filter(p=>p!==plane)
}
// po.lookAt(
// po.position.x - plane.normal.x,
// po.position.y - plane.normal.y,
// po.position.z - plane.normal.z,
// );
// if(!this.helpers[i]){
// this.helpers[i]=new THREE.Mesh(new THREE.PlaneGeometry(max,max),new THREE.MeshBasicMaterial({
// color:[0xff000,0xffff00,0x0000ff,0x00ffff,0x00ff00,0xff00ff][i],
// //side:THREE.DoubleSide,
// depthTest:true,
// }))
// this.clipScene.add(this.helpers[i])
// }
// this.helpers[i].position.set(0,0,0)
// this.helpers[i].lookAt(plane.normal.clone().negate())
// this.helpers[i].position.copy(plane.normal).multiplyScalar(targetDist[i])
// this.helpers[i].position.add(center)
// po.position.set(0,0,0)
// po.lookAt(plane.normal.clone().negate())
po.position.copy(plane.normal).multiplyScalar(targetDist[i])
po.position.add(center)
po.lookAt(
po.position.x - plane.normal.x,
po.position.y - plane.normal.y,
po.position.z - plane.normal.z,
);
po.updateMatrixWorld()
}
}
helpers=[]
negativeClip(){
this.options.negative=!this.options.negative;
this.updatePlanes()
this.updateClipCap()
this.updateTargetClipPlanes()
this.update()
}
}
export class BoxSliceHandle extends BaseHandle<{},{
'value-change':{}
}>{
static handleName: HANDLE_TYPES=HANDLE_TYPES.CUTTING_BOX;
clipBox?:ClipBox;
init(): void {
this.clipBox=new ClipBox({
renderer:this.renderer.renderer,
scene:this.renderer.scene,
camera:()=>this.renderer.camera,
onDragStart:()=>{
this.renderer.disableControls()
},
onDragEnd:()=>{
this.renderer.enableControls()
},
update:()=>{
this.renderer.refresh()
},
onDragRange:()=>{
this.dispatchEvent({type:'value-change'})
}
})
}
resetClip(){
this.clipBox?.reset()
}
setBoxClipValue(axes:string,min:number,max:number){
const clipBox=this.clipBox!;
if(axes==='x'){
clipBox.min.x=min
clipBox.max.x=max
}else if(axes==='y'){
clipBox.min.y=min
clipBox.max.y=max
}else if(axes==='z'){
clipBox.min.z=min
clipBox.max.z=max
}
this.clipBox?.updateClipBoxFromAxes(axes)
this.refresh()
}
getBoxClipValue(){
const clipBox=this.clipBox!;
return [clipBox.min.clone(),clipBox.max.clone()]
}
getBoxClipRange(){
const clipBox=this.clipBox!;
return [clipBox.initMin.clone(),clipBox.initMax.clone()]
}
onEnter(options?: {} | undefined): void {
this.clipBox?.attach(this.renderer.modelScene)
this.clipBox?.showClip()
}
onLeave(): void {
this.clipBox?.hideClip()
}
exitBoxClip(){
this.clipBox?.exit()
this.ownerHandle.switchNone()
}
dispose(): void {
}
handlePointer(e: { target: MouseHandler; type: string;intersections:THREE.Intersection[] }): void {
if(e.type==='object-enter'){
// e.intersections[0]
}else if(e.type==='pointerup'){
//this.switch(HANDLE_TYPES.NONE)
}
}
}