@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
268 lines (245 loc) • 8.24 kB
text/typescript
/**
* Snaps points onto one another.
*
* @remarks
* Based on a distance threshold.
*
*/
import {TypedSopNode} from './_Base';
import {CoreGroup} from '../../../core/geometry/Group';
import {BufferAttribute, BufferGeometry, Object3D, Vector2, Vector3, Vector4, Mesh} from 'three';
import type {Points, LineSegments} from 'three';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {InputCloneMode} from '../../poly/InputCloneMode';
import {pushOnArrayAtEntry} from '../../../core/MapUtils';
import {ObjectType, objectTypeFromObject} from '../../../core/geometry/Constant';
import {arrayUniq} from '../../../core/ArrayUtils';
import {mergeFaces, mergeFacesWithUnsharedEdges} from '../../../core/geometry/operation/Fuse';
import {CoreMask} from '../../../core/geometry/Mask';
import {SopType} from '../../poly/registers/nodes/types/Sop';
import {Attribute} from '../../../core/geometry/Attribute';
const roundedPosition = new Vector3();
const vector2 = new Vector2();
const vector3 = new Vector3();
const vector4 = new Vector4();
function clearAttributes(geometry: BufferGeometry) {
const attributeNames = Object.keys(geometry.attributes);
for (const attributeName of attributeNames) {
const attribute = geometry.getAttribute(attributeName);
if (attribute instanceof BufferAttribute) {
const newAttribValues: number[] = [];
geometry.setAttribute(
attributeName,
new BufferAttribute(new Float32Array(newAttribValues), attribute.itemSize)
);
}
}
}
class FuseSopParamsConfig extends NodeParamsConfig {
/** @param group to assign the material to */
group = ParamConfig.STRING('', {
objectMask: true,
});
/** @param distance threshold */
dist = ParamConfig.FLOAT(0.001, {
range: [0, 1],
rangeLocked: [true, false],
step: 0.001,
});
/** @param fuse only open edges */
onlyOpenEdges = ParamConfig.BOOLEAN(false);
/** @param recompute normals */
computeNormals = ParamConfig.BOOLEAN(1);
}
const ParamsConfig = new FuseSopParamsConfig();
export class FuseSopNode extends TypedSopNode<FuseSopParamsConfig> {
override paramsConfig = ParamsConfig;
static override type() {
return SopType.FUSE;
}
override initializeNode() {
this.io.inputs.setCount(1);
this.io.inputs.initInputsClonedState(InputCloneMode.FROM_NODE);
}
override cook(inputCoreGroups: CoreGroup[]) {
const inputCoreGroup = inputCoreGroups[0];
const selectedObjects = CoreMask.filterThreejsObjects(inputCoreGroup, this.pv);
for (const object of selectedObjects) {
this._filterObject(object);
}
this.setCoreGroup(inputCoreGroup);
}
private _filterObject(object: Object3D) {
const objectType = objectTypeFromObject(object);
switch (objectType) {
case ObjectType.MESH: {
return this._filterMesh(object as Mesh);
}
case ObjectType.LINE_SEGMENTS: {
this._fuseGeometry((object as LineSegments).geometry);
return this._filterLineSegments(object as LineSegments);
}
case ObjectType.POINTS: {
this._fuseGeometry((object as Points).geometry);
return this._filterPoints(object as Points);
}
}
}
private _filterMesh(object: Mesh) {
const geometry = object.geometry;
const index = geometry.getIndex();
if (!index) {
return;
}
if (this.pv.onlyOpenEdges == true) {
mergeFacesWithUnsharedEdges(geometry, this.pv.dist);
} else {
mergeFaces(geometry, this.pv.dist);
}
}
private _filterLineSegments(object: LineSegments) {
const geometry = object.geometry;
const index = geometry.getIndex();
if (!index) {
return;
}
const newIndices: number[] = [];
const indexArray = index.array;
const segmentsCount = indexArray.length / 2;
for (let i = 0; i < segmentsCount; i++) {
vector2.fromArray(indexArray, i * 2);
const a = vector2.x;
const b = vector2.y;
const segmentSnapped = a == b;
if (!segmentSnapped) {
vector2.toArray(newIndices, newIndices.length);
}
}
geometry.setIndex(newIndices);
if (newIndices.length == 0) {
clearAttributes(geometry);
}
}
private _filterPoints(object: Points) {
const geometry = object.geometry;
const index = geometry.getIndex();
if (!index) {
return;
}
const indexArray = [...index.array];
const newIndices: number[] = [];
arrayUniq(indexArray, newIndices);
newIndices.sort((a, b) => a - b);
geometry.setIndex(newIndices);
if (newIndices.length == 0) {
clearAttributes(geometry);
}
}
private _fuseGeometry(geometry: BufferGeometry) {
const index = geometry.getIndex();
if (!index) {
return;
}
const indexArray = index.array;
const precision = this.pv.dist;
const position = geometry.getAttribute(Attribute.POSITION) as BufferAttribute;
const srcPointsCount = position.array.length / 3;
function roundedPos(index: number, target: Vector3) {
target.fromBufferAttribute(position, index);
if (precision > 0) {
target.x = Math.round(target.x / precision) * precision;
target.y = Math.round(target.y / precision) * precision;
target.z = Math.round(target.z / precision) * precision;
}
}
const indicesByPosKey: Map<string, Array<number>> = new Map();
const posKeyByIndex: Map<number, string> = new Map();
for (let index = 0; index < srcPointsCount; index++) {
roundedPos(index, roundedPosition);
const posKey = `${roundedPosition.x},${roundedPosition.y},${roundedPosition.z}`;
pushOnArrayAtEntry(indicesByPosKey, posKey, index);
posKeyByIndex.set(index, posKey);
}
indicesByPosKey.forEach((indices, posKey) => {
indices.sort((a, b) => a - b);
});
const newIndicesAfterGapsCreated: Map<number, number> = new Map();
let nextAvailableIndex = 0;
for (let index = 0; index < srcPointsCount; index++) {
const posKey = posKeyByIndex.get(index)!;
const indices = indicesByPosKey.get(posKey)!;
if (indices.length <= 1 || indices[0] == index) {
newIndicesAfterGapsCreated.set(index, nextAvailableIndex);
nextAvailableIndex++;
}
}
const newIndexByOldIndex: Map<number, number> = new Map();
indicesByPosKey.forEach((indices, posKey) => {
const firstIndex = indices[0];
for (let i = 1; i < indices.length; i++) {
const index = indices[i];
newIndexByOldIndex.set(index, firstIndex);
}
});
const newIndices: number[] = [];
const newIndexByOldIndexAfterAssignment: Map<number, number> = new Map();
for (let i = 0; i < indexArray.length; i++) {
const index = indexArray[i];
const targetIndex = newIndexByOldIndex.get(index);
// const offsetIndex = newIndicesAfterGapsCreated.get(index);
const targetOffset =
targetIndex != null
? newIndicesAfterGapsCreated.get(targetIndex)
: newIndicesAfterGapsCreated.get(index);
// const offset2 = offsetIndex != null ? newIndexByOldIndex.get(offsetIndex) : undefined;
let newIndex = index;
if (targetOffset != null) {
newIndex = targetOffset;
} else {
if (targetIndex != null) {
newIndex = targetIndex;
}
}
newIndices.push(newIndex);
newIndexByOldIndexAfterAssignment.set(index, newIndex);
}
const attributeNames = Object.keys(geometry.attributes);
for (const attributeName of attributeNames) {
const attribute = geometry.getAttribute(attributeName);
if (attribute instanceof BufferAttribute) {
const itemSize = attribute.itemSize;
const newAttribValues: number[] = [];
function getVector() {
if (itemSize == 2) {
return vector2;
}
if (itemSize == 3) {
return vector3;
}
if (itemSize == 4) {
return vector4;
}
}
const vector = getVector();
const visitedIndex: Set<number> = new Set();
for (let i = 0; i < srcPointsCount; i++) {
const index = newIndexByOldIndexAfterAssignment.get(i);
if (index != null) {
if (!visitedIndex.has(index)) {
visitedIndex.add(index);
if (vector) {
vector.fromBufferAttribute(attribute, i);
vector.toArray(newAttribValues, index * itemSize);
} else {
const currentVal = attribute.array[i];
newAttribValues[index] = currentVal;
}
}
}
}
geometry.setAttribute(attributeName, new BufferAttribute(new Float32Array(newAttribValues), itemSize));
}
}
geometry.setIndex(newIndices);
}
}