src/main/java/mesh/modifier/BevelEdgesModifier.java
package mesh.modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import math.Mathf;
import math.Vector3f;
import mesh.Edge3D;
import mesh.Face3D;
import mesh.Mesh3D;
import mesh.util.TraverseHelper;
public class BevelEdgesModifier implements IMeshModifier {
public enum WidthType {
OFFSET, WIDTH, DEPTH
}
private float inset;
private float amount;
private WidthType widthType = WidthType.OFFSET;
private Mesh3D mesh;
private List<Face3D> facesToAdd;
private List<Vector3f> verticesToAdd;
private EdgeProcessor edgeProcessor;
public BevelEdgesModifier(float amount) {
setAmount(amount);
edgeProcessor = new EdgeProcessor();
verticesToAdd = new ArrayList<Vector3f>();
facesToAdd = new ArrayList<Face3D>();
}
public BevelEdgesModifier() {
this(0.1f);
}
@Override
public Mesh3D modify(Mesh3D mesh) {
validateMesh(mesh);
if (canExitEarly(mesh)) {
return mesh;
}
setMesh(mesh);
clearAll();
createInsetFaces();
createFacesForOldEdges();
createFacesVertex();
clearOriginalFaces();
clearOriginalVertices();
addNewVertices();
addNewFaces();
return mesh;
}
private void createFacesVertex() {
TraverseHelper helper = new TraverseHelper(mesh);
for (int i = 0; i < mesh.getVertexCount(); i++) {
Edge3D outgoingEdge = helper.getOutgoing(i);
Edge3D edge = outgoingEdge;
List<Integer> indices = new ArrayList<Integer>();
do {
Edge3D newEdge = edgeProcessor.getMappedEdge(edge);
int index = newEdge.fromIndex;
indices.add(index);
edge = helper.getPairNext(edge.fromIndex, edge.toIndex);
} while (!outgoingEdge.equals(edge));
facesToAdd.add(new Face3D(toReverseArray(indices)));
}
}
private void createFacesForOldEdges() {
for (Face3D face : mesh.faces)
for (int i = 0; i < face.indices.length; i++)
createFaceForOldEdgeAt(face, i);
}
private void createFaceForOldEdgeAt(Face3D face, int i) {
Edge3D edge = edgeProcessor
.getMappedEdge(edgeProcessor.createEdge(face.indices, i));
Edge3D pair = edgeProcessor
.getMappedEdge(edgeProcessor.createEdge(face.indices, i).createPair());
if (edgeProcessor.isProcessed(edge) || edgeProcessor.isProcessed(pair))
return;
createFaceForEdge(edge, pair);
edgeProcessor.markProcessed(edge);
edgeProcessor.markProcessed(pair);
}
private void createFaceForEdge(Edge3D edge, Edge3D pair) {
addNewFace(edge.toIndex, edge.fromIndex, pair.toIndex, pair.fromIndex);
}
private int[] toReverseArray(List<Integer> values) {
Collections.reverse(values);
return values.stream().mapToInt(x -> x).toArray();
}
private void clearAll() {
edgeProcessor.clearAll();
verticesToAdd.clear();
facesToAdd.clear();
}
private void createInsetFaces() {
for (Face3D face : mesh.faces)
insetFace(mesh, face);
}
private void insetFace(Mesh3D mesh, Face3D face) {
int nextVertexIndex = verticesToAdd.size();
int[] indices = createIndices(face.indices.length, nextVertexIndex);
createInsetVertices(processFaceEdges(face));
edgeProcessor.mapOldEdgesToNewEdges(face, indices);
addNewFace(indices);
}
private int[] createIndices(int size, int nextVertexIndex) {
int[] indices = new int[size];
for (int i = 0; i < indices.length; i++)
indices[i] = nextVertexIndex + i;
return indices;
}
/**
* Processes the edges of a face to calculate the new inset vertices.
*
* @param face the face to process.
* @return a list of inset vertices.
*/
private List<Vector3f> processFaceEdges(Face3D face) {
List<Vector3f> vertices = new ArrayList<>();
for (int i = 0; i < face.indices.length; i++) {
Vector3f from = getVertexAt(face, i);
Vector3f to = getVertexAt(face, i + 1);
float edgeLength = to.distance(from);
float insetFactor = calculateInsetFactor(edgeLength);
Vector3f v4 = to.subtract(from).mult(insetFactor).add(from);
Vector3f v5 = to.add(to.subtract(from).mult(-insetFactor));
vertices.add(v4);
vertices.add(v5);
}
return vertices;
}
/**
* Creates the inset vertices from the processed edge vertices.
*
* @param vertices the processed edge vertices.
*/
private void createInsetVertices(List<Vector3f> vertices) {
for (int i = 1; i < vertices.size(); i += 2) {
int a = vertices.size() - 2 + i;
Vector3f v0 = vertices.get(a % vertices.size());
Vector3f v1 = vertices.get((a + 1) % vertices.size());
Vector3f v = v1.add(v0).mult(0.5f);
verticesToAdd.add(v);
}
}
/**
* Calculates the inset factor based on the edge length and
* #{@link WidthType}.
*
* @param edgeLength the length of the edge.
* @return the inset factor.
*/
private float calculateInsetFactor(float edgeLength) {
return edgeLength > 0 ? (1f / edgeLength) * getAmountByWidthType() : 0f;
}
private float getAmountByWidthType() {
float amount;
switch (widthType) {
case OFFSET -> amount = this.amount * 2;
case WIDTH -> amount = inset;
case DEPTH -> amount = inset * 2;
default -> amount = this.amount * 2;
}
return amount;
}
private boolean canExitEarly(Mesh3D mesh) {
return amount == 0 || mesh.faces.isEmpty();
}
private void validateMesh(Mesh3D mesh) {
if (mesh == null) {
throw new IllegalArgumentException("Mesh cannot be null.");
}
}
private Vector3f getVertexAt(Face3D face, int index) {
return mesh.getVertexAt(face.indices[index % face.indices.length]);
}
private void addNewVertices() {
mesh.vertices.addAll(verticesToAdd);
}
private void addNewFaces() {
mesh.faces.addAll(facesToAdd);
}
private void clearOriginalVertices() {
mesh.vertices.clear();
}
private void clearOriginalFaces() {
mesh.faces.clear();
}
private void addNewFace(int... indices) {
facesToAdd.add(new Face3D(indices));
}
private void setMesh(Mesh3D mesh) {
this.mesh = mesh;
}
public void setAmount(float amount) {
this.amount = amount;
updateInset();
}
private void updateInset() {
inset = Mathf.sqrt((amount * amount) + (amount * amount));
}
public WidthType getWidthType() {
return widthType;
}
public void setWidthType(WidthType widthType) {
this.widthType = widthType;
}
private class EdgeProcessor {
private HashSet<Edge3D> processed;
private HashMap<Edge3D, Edge3D> edgeMapping;
public EdgeProcessor() {
processed = new HashSet<Edge3D>();
edgeMapping = new HashMap<Edge3D, Edge3D>();
}
public void mapOldEdgesToNewEdges(Face3D face, int[] indices) {
for (int i = 0; i < indices.length; i++) {
Edge3D oldEdge = createEdge(face.indices, i);
Edge3D newEdge = createEdge(indices, i);
edgeMapping.put(oldEdge, newEdge);
}
}
public Edge3D getMappedEdge(Edge3D edge) {
return edgeMapping.get(edge);
}
public void markProcessed(Edge3D edge) {
processed.add(edge);
}
public boolean isProcessed(Edge3D edge) {
return processed.contains(edge);
}
public Edge3D createEdge(int[] indices, int i) {
return new Edge3D(indices[i], indices[(i + 1) % indices.length]);
}
public void clearAll() {
processed.clear();
edgeMapping.clear();
}
}
}