@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
258 lines (233 loc) • 8.79 kB
text/typescript
import {BaseSopOperation} from './_Base';
import {CoreGroup} from '../../../core/geometry/Group';
import {
Box3,
InterleavedBufferAttribute,
BufferAttribute,
BufferGeometry,
LineSegments,
Mesh,
Line3,
BoxGeometry,
Object3D,
Vector3,
Plane,
} from 'three';
import {InputCloneMode} from '../../../engine/poly/InputCloneMode';
import {MeshWithBVH, ExtendedTriangle} from '../../../core/geometry/bvh/three-mesh-bvh';
import {DEFAULT_MATERIALS, ObjectType} from '../../../core/geometry/Constant';
import {DefaultOperationParams} from '../../../core/operations/_Base';
import {ThreeMeshBVHHelper} from '../../../core/geometry/bvh/ThreeMeshBVHHelper';
import {isBooleanTrue} from '../../../core/Type';
import {SUBTRACTION, Brush, Evaluator} from '../../../core/thirdParty/three-bvh-csg';
import {rotateGeometry} from '../../../core/Transform';
import {CoreGeometryBuilderMesh} from '../../../core/geometry/modules/three/builders/Mesh';
import {corePointClassFactory} from '../../../core/geometry/CoreObjectFactory';
import {pointsFromObject} from '../../../core/geometry/entities/point/CorePointUtils';
import {CorePoint} from '../../../core/geometry/entities/point/CorePoint';
import {CoreObjectType} from '../../../core/geometry/ObjectContent';
import {copyObject3DProperties} from '../../../core/geometry/modules/three/ThreejsObjectUtils';
interface ClipSopParams extends DefaultOperationParams {
origin: Vector3;
distance: number;
direction: Vector3;
intersectionEdges: boolean;
keepBelowPlane: boolean;
keepAbovePlane: boolean;
}
const tempVector = new Vector3();
const tempLine = new Line3();
const _plane = new Plane();
const DEFAULT_UP = new Vector3(0, 1, 0);
const TMP_KEEP_ATTRIBUTE_NAME = '___keep___';
const TMP_KEEP_ATTRIBUTE_SIZE = 1;
const objectsToRemove: Set<Object3D> = new Set();
// const eye = new Vector3(0, 0, 0);
// const UP = new Vector3(0, 1, 0);
// const _matT = new Matrix4();
// const _matR = new Matrix4();
// const _mat = new Matrix4();
const BOOLEAN_SIZE = 10000;
const _points: CorePoint<CoreObjectType>[] = [];
export class ClipSopOperation extends BaseSopOperation {
static override readonly DEFAULT_PARAMS: ClipSopParams = {
origin: new Vector3(0, 0, 0),
distance: 0,
direction: new Vector3(0, 1, 0),
intersectionEdges: false,
keepBelowPlane: true,
keepAbovePlane: false,
};
static override readonly INPUT_CLONED_STATE = InputCloneMode.ALWAYS;
static override type(): Readonly<'clip'> {
return 'clip';
}
override cook(inputCoreGroups: CoreGroup[], params: ClipSopParams) {
const coreGroup = inputCoreGroups[0];
const newObjects: Object3D[] = [];
_plane.set(params.direction, -params.distance);
_plane.translate(params.origin);
const inputObjects = coreGroup.threejsObjects();
objectsToRemove.clear();
for (const object of inputObjects) {
object.traverse((child) => {
_processObjectClipped(child, params, newObjects);
});
}
objectsToRemove.forEach((child) => {
child.removeFromParent();
});
return this.createCoreGroupFromObjects(newObjects);
}
}
function _processObjectClipped(object: Object3D, params: ClipSopParams, newObjects: Object3D[]) {
const geometry = (object as Mesh).geometry;
if (!geometry) {
return;
}
const mesh = object as Mesh;
function _addObject(newObject: Object3D) {
newObjects.push(newObject);
copyObject3DProperties(object, newObject);
objectsToRemove.add(object);
}
if (isBooleanTrue(params.keepBelowPlane) || isBooleanTrue(params.keepAbovePlane)) {
if (isBooleanTrue(params.keepBelowPlane)) {
const box = _createBox(params, true);
const belowPlane = _createClipped(mesh, box);
_addObject(belowPlane);
}
if (isBooleanTrue(params.keepAbovePlane)) {
const box = _createBox(params, false);
const abovePlane = _createClipped(mesh, box);
_addObject(abovePlane);
}
}
if (isBooleanTrue(params.intersectionEdges)) {
const intersectionEdges = _createClipGeo(mesh);
if (intersectionEdges) {
const oldMaterial = intersectionEdges.material;
_addObject(intersectionEdges);
intersectionEdges.material = oldMaterial;
}
}
}
function _createClipped(mesh: Mesh, box: Mesh) {
const csgEvaluator = new Evaluator();
const corePointClass = corePointClassFactory(mesh);
// add keep attribute for mesh (value=1)
function _addKeepAttribute(object: Mesh, value: number) {
corePointClass.addNumericAttribute(object, TMP_KEEP_ATTRIBUTE_NAME, TMP_KEEP_ATTRIBUTE_SIZE, value);
}
_addKeepAttribute(mesh, 1);
_addKeepAttribute(box, 0);
// perform boolean operation
const brush1 = new Brush(mesh.geometry, mesh.material);
const brush2 = new Brush(box.geometry);
const existingAttributes = corePointClass.attributeNames(mesh);
csgEvaluator.attributes = [...existingAttributes, TMP_KEEP_ATTRIBUTE_NAME];
const output = csgEvaluator.evaluate(brush1, brush2, SUBTRACTION);
output.disposeCacheData();
BaseSopOperation.createIndexIfNone(output.geometry);
// delete attributes where keep attribute == 0
// const outCoreGeometry = new CoreGeometry(output.geometry);
pointsFromObject(output, _points);
const keptPoints = _points.filter((p) => p.attribValue(TMP_KEEP_ATTRIBUTE_NAME) == 1);
const builder = new CoreGeometryBuilderMesh();
const newGeometry = builder.fromPoints(output, keptPoints);
const object = BaseSopOperation.createObject(newGeometry, ObjectType.MESH);
return object;
}
function _createBox(params: ClipSopParams, above: boolean) {
const {origin, direction, distance} = params;
const geometry = new BoxGeometry(BOOLEAN_SIZE, BOOLEAN_SIZE, BOOLEAN_SIZE, 2, 2, 2);
geometry.translate(0, BOOLEAN_SIZE * 0.5 * (above ? 1 : -1), 0);
geometry.translate(origin.x, origin.y + distance, origin.z);
rotateGeometry(geometry, DEFAULT_UP, direction);
// geometry.lookAt(direction);
const object = BaseSopOperation.createObject(geometry, ObjectType.MESH);
// _matT.identity();
// _matR.identity();
// _mat.identity();
// _matT.makeTranslation(origin.x, origin.y + distance, origin.z);
// _matR.lookAt(eye, direction, UP);
// _mat.multiplyMatrices(_matT, _matR);
// object.matrix.copy(_mat);
return object;
}
function _createClipGeo(mesh: Mesh) {
const meshBVH = mesh as MeshWithBVH;
let bvh = meshBVH.geometry.boundsTree;
if (!bvh) {
ThreeMeshBVHHelper.assignDefaultBVHIfNone(mesh);
bvh = meshBVH.geometry.boundsTree;
}
const performIntersection = (posAttrib?: BufferAttribute | InterleavedBufferAttribute) => {
let index = 0;
const intersectsBounds = (
box: Box3,
isLeaf: boolean,
score: number | undefined,
depth: number,
nodeIndex: number
) => {
return _plane.intersectsBox(box);
};
const intersectsTriangle = (tri: ExtendedTriangle) => {
// check each triangle edge to see if it intersects with the plane. If so then
// add it to the list of segments.
let count = 0;
tempLine.start.copy(tri.a);
tempLine.end.copy(tri.b);
if (_plane.intersectLine(tempLine, tempVector)) {
posAttrib?.setXYZ(index, tempVector.x, tempVector.y, tempVector.z);
index++;
count++;
}
tempLine.start.copy(tri.b);
tempLine.end.copy(tri.c);
if (_plane.intersectLine(tempLine, tempVector)) {
posAttrib?.setXYZ(index, tempVector.x, tempVector.y, tempVector.z);
count++;
index++;
}
tempLine.start.copy(tri.c);
tempLine.end.copy(tri.a);
if (_plane.intersectLine(tempLine, tempVector)) {
posAttrib?.setXYZ(index, tempVector.x, tempVector.y, tempVector.z);
count++;
index++;
}
// If we only intersected with one or three sides then just remove it. This could be handled
// more gracefully.
if (count !== 2) {
index -= count;
}
};
bvh.shapecast({
intersectsBounds,
intersectsTriangle,
});
return {index};
};
// we perform the intersection once to know the required size of the position attribute
const {index} = performIntersection();
const lineGeometry = new BufferGeometry();
const linePosAttr = new BufferAttribute(new Float32Array(index * 3), 3, false);
// No index is added for now, as this would add internal lines.
// But this means that this geometry cannot yet be processed by operations that require indices,
// such as sop/polywire.
// const indices: number[] = new Array(index * 2);
// for (let i = 0; i < index - 1; i++) {
// indices[i * 2] = i;
// indices[i * 2 + 1] = i + 1;
// }
// lineGeometry.setIndex(indices);
lineGeometry.setAttribute('position', linePosAttr);
const outlineLines = new LineSegments(lineGeometry, DEFAULT_MATERIALS[ObjectType.LINE_SEGMENTS]);
outlineLines.frustumCulled = false;
const posAttr = outlineLines.geometry.attributes.position as BufferAttribute;
// and we perform the intersection a second time to actually set the position attribute values
performIntersection(posAttr);
return outlineLines;
}