diff --git a/src/main/java/math/Color.java b/src/main/java/math/Color.java index c6b0ffab..cc1613bf 100644 --- a/src/main/java/math/Color.java +++ b/src/main/java/math/Color.java @@ -171,9 +171,9 @@ public Color add(Color color) { /** * Adds the components of a given color to those of this color storing the - * result in the given result color. Each component is added separately. If the - * provided color c is null, an exception is thrown. If the provided result - * color is null, a new color is created. + * result in the given result color. Each component is added separately. If + * the provided color c is null, an exception is thrown. If the provided + * result color is null, a new color is created. * * @param color the color to add to this color * @param result the color to store the result in @@ -190,8 +190,8 @@ public Color add(Color color, Color result) { } /** - * Adds the given r,g,b,a components to those of this color creating a new color - * object. Each component is added separately. + * Adds the given r,g,b,a components to those of this color creating a new + * color object. Each component is added separately. * * @param r the red component to add * @param g the green component to add @@ -204,8 +204,8 @@ public Color add(float r, float g, float b, float a) { } /** - * Adds the color c to this color internally, and returns a handle to this color - * for easy chaining of calls. Each component is added separately. + * Adds the color c to this color internally, and returns a handle to this + * color for easy chaining of calls. Each component is added separately. * * @param color the color to add to this color * @return this @@ -238,9 +238,9 @@ public Color addLocal(float r, float g, float b, float a) { } /** - * Subtracts the components of a given color from those of this color creating a - * new color object. Each component is subtracted separately. If the provided - * color is null, an exception is thrown. + * Subtracts the components of a given color from those of this color creating + * a new color object. Each component is subtracted separately. If the + * provided color is null, an exception is thrown. * * @param color the color to subtract from this color * @return the resultant color @@ -270,8 +270,8 @@ public Color subtract(Color color, Color result) { } /** - * * Subtracts the given r,g,b,a components from those of this color creating a - * new color object. Each component is subtracted separately. + * * Subtracts the given r,g,b,a components from those of this color creating + * a new color object. Each component is subtracted separately. * * @param r the red component to subtract * @param g the green component to subtract @@ -347,8 +347,8 @@ public Color clampLocal() { } /** - * Sets all components of this color to 0.0f internally, and returns a handle to - * this color for easy chaining of calls. + * Sets all components of this color to 0.0f internally, and returns a handle + * to this color for easy chaining of calls. * * @return this */ @@ -368,8 +368,8 @@ public float maxComponent() { } /** - * Returns a new float array containing the r,g,b,a components of this color in - * that order. + * Returns a new float array containing the r,g,b,a components of this color + * in that order. * * @return the components of this color as array */ @@ -378,8 +378,8 @@ public float[] toArray() { } /** - * Stores the r,g,b,a components in the given array. If the provided store array - * is null a new array is created to store the components in. + * Stores the r,g,b,a components in the given array. If the provided store + * array is null a new array is created to store the components in. * * @param store the array to store the components into * @return store @@ -508,12 +508,14 @@ public int getRGBA() { int g = getGreenInt(); int b = getBlueInt(); int a = getAlphaInt(); - return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | ((b & 0xFF) << 0); + return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) + | ((b & 0xFF) << 0); } /** - * Returns a unique hash code for this color object based on it's values. If two - * colors are logically equivalent, they will return the same hash code value. + * Returns a unique hash code for this color object based on it's values. If + * two colors are logically equivalent, they will return the same hash code + * value. * * @return the hash code value of this color */ @@ -544,9 +546,9 @@ public boolean equals(Object obj) { return false; Color other = (Color) obj; return Float.floatToIntBits(r) == Float.floatToIntBits(other.r) - && Float.floatToIntBits(g) == Float.floatToIntBits(other.g) - && Float.floatToIntBits(b) == Float.floatToIntBits(other.b) - && Float.floatToIntBits(a) == Float.floatToIntBits(other.a); + && Float.floatToIntBits(g) == Float.floatToIntBits(other.g) + && Float.floatToIntBits(b) == Float.floatToIntBits(other.b) + && Float.floatToIntBits(a) == Float.floatToIntBits(other.a); } /** diff --git a/src/main/java/math/Mathf.java b/src/main/java/math/Mathf.java index 6f815973..70c06d16 100644 --- a/src/main/java/math/Mathf.java +++ b/src/main/java/math/Mathf.java @@ -113,7 +113,8 @@ public class Mathf { /** * The Tribonacci constant, often denoted as t, is the real root of the cubic - * equation x³ - x² - x - 1 = 0. It is approximately equal to 1.83928675521416. + * equation x³ - x² - x - 1 = 0. It is approximately equal to + * 1.83928675521416. */ public static final float TRIBONACCI_CONSTANT = 1.83928675521416f; @@ -121,20 +122,21 @@ public class Mathf { * Converts a 2D index (row, column) into a 1D index for a matrix or array. * *
- * This method is useful when working with matrices or arrays that are stored in - * a 1D array. It calculates the 1D index corresponding to the specified row and - * column in a matrix with the given number of columns. + * This method is useful when working with matrices or arrays that are stored + * in a 1D array. It calculates the 1D index corresponding to the specified + * row and column in a matrix with the given number of columns. * * @param rowIndex The zero-based index of the row. * @param colIndex The zero-based index of the column. * @param numberOfColumns The total number of columns in the matrix. * @return The 1D index corresponding to the given row and column. * - * @throws IllegalArgumentException if `rowIndex` or `colIndex` is negative, or - * if `numberOfColumns` is less than or equal - * to zero. + * @throws IllegalArgumentException if `rowIndex` or `colIndex` is negative, + * or if `numberOfColumns` is less than or + * equal to zero. */ - public static int toOneDimensionalIndex(int rowIndex, int colIndex, int numberOfColumns) { + public static int toOneDimensionalIndex(int rowIndex, int colIndex, + int numberOfColumns) { if (rowIndex < 0 || colIndex < 0) throw new IllegalArgumentException(); @@ -228,8 +230,8 @@ public static float min(float a, float b) { * Returns the maximum float value in the given array. * * @param values The array of float values. - * @return The maximum value in the array, or {@link Float#NaN} if the array is - * empty. + * @return The maximum value in the array, or {@link Float#NaN} if the array + * is empty. */ public static float max(float... values) { if (values.length == 0) @@ -245,8 +247,8 @@ public static float max(float... values) { * Returns the minimum float value in the given array. * * @param values The array of float values. - * @return The minimum value in the array, or {@link Float#NaN} if the array is - * empty. + * @return The minimum value in the array, or {@link Float#NaN} if the array + * is empty. */ public static float min(float... values) { if (values.length == 0) @@ -274,8 +276,8 @@ public static int roundToInt(float a) { * *
* This method rounds the given float value to the nearest integer. If the - * fractional part is 0.5 or greater, the value is rounded up. Otherwise, it is - * rounded down. + * fractional part is 0.5 or greater, the value is rounded up. Otherwise, it + * is rounded down. * * @param a The float value to be rounded. * @return The rounded float value. @@ -305,13 +307,12 @@ public static float clamp(float a, float min, float max) { * @return The clamped value. */ public static int clampInt(int a, int min, int max) { - a = a < min ? min : (a > max ? max : a); - return a; + return a < min ? min : (a > max ? max : a); } /** - * Clamps the given float value to be between 0 and 1. This method is equivalent - * to {@link #saturate(float)}. + * Clamps the given float value to be between 0 and 1. This method is + * equivalent to {@link #saturate(float)}. * * @param a The value to clamp. * @return A clamped value between 0 and 1- @@ -369,8 +370,8 @@ public static float abs(float a) { * Returns the trigonometric tangent of an angle. Special cases: *
* When t = 0 returns a.
@@ -694,8 +696,8 @@ public static float lerpUnclamped(float a, float b, float t) {
}
/**
- * Linearly interpolates between from and to by t. The parameter t is clamped to
- * the range [0, 1].
+ * Linearly interpolates between from and to by t. The parameter t is clamped
+ * to the range [0, 1].
*
*
* When t = 0 returns a.
@@ -734,8 +736,8 @@ public static int nextPowerOfTwo(int value) {
/**
* Smoothly interpolates between two values. This function provides a smoother
- * transition between the two values compared to linear interpolation. It uses a
- * cubic Hermite spline to achieve a smooth curve.
+ * transition between the two values compared to linear interpolation. It uses
+ * a cubic Hermite spline to achieve a smooth curve.
*
* @param from The starting value.
* @param to The ending value.
@@ -795,16 +797,17 @@ public static float randomFloat() {
}
/**
- * Calculates a smooth, oscillating value between 0 and `length` over time `t`.
+ * Calculates a smooth, oscillating value between 0 and `length` over time
+ * `t`.
*
- * This function is commonly used in game development to create various effects,
- * such as character movement, object animations, camera effects, and particle
- * systems.
+ * This function is commonly used in game development to create various
+ * effects, such as character movement, object animations, camera effects, and
+ * particle systems.
*
* The function works by repeating the input time `t` over an interval of
- * `length * 2`, and then calculating the distance between the repeated time and
- * the midpoint `length`. This distance is then subtracted from `length` to
- * produce the final oscillating value.
+ * `length * 2`, and then calculating the distance between the repeated time
+ * and the midpoint `length`. This distance is then subtracted from `length`
+ * to produce the final oscillating value.
*
* @param t The input time.
* @param length The desired range of oscillation.
@@ -816,7 +819,8 @@ public static float pingPong(float t, float length) {
}
/**
- * Normalizes an angle to a specific range centered around a given center angle.
+ * Normalizes an angle to a specific range centered around a given center
+ * angle.
*
* This method ensures that the returned angle is within a specific range,
* typically between -π and π or 0 and 2π.
@@ -832,14 +836,14 @@ public static float normalizeAngle(float a, float center) {
/**
* Wraps a value cyclically within a specified range.
*
- * This method takes a value `t` and maps it to a value within the interval [0,
- * length). The value is repeatedly decreased by `length` until it becomes less
- * than `length`. This creates a cyclic effect, where the value continuously
- * cycles from 0 to `length` and then back to 0.
+ * This method takes a value `t` and maps it to a value within the interval
+ * [0, length). The value is repeatedly decreased by `length` until it becomes
+ * less than `length`. This creates a cyclic effect, where the value
+ * continuously cycles from 0 to `length` and then back to 0.
*
- * **Example:** For `t = 12` and `length = 5`, the result is: - `floor(12 / 5) =
- * 2` (number of full cycles) - `2 * 5 = 10` (value exceeding the range) - `12 -
- * 10 = 2` (the returned value)
+ * **Example:** For `t = 12` and `length = 5`, the result is: - `floor(12 / 5)
+ * = 2` (number of full cycles) - `2 * 5 = 10` (value exceeding the range) -
+ * `12 - 10 = 2` (the returned value)
*
* @param t The value to be wrapped.
* @param length The length of the interval within which the value is wrapped.
@@ -852,18 +856,18 @@ public static float repeat(float t, float length) {
/**
* Determines if two floating-point numbers are approximately equal.
*
- * This method compares two floating-point numbers, `a` and `b`, considering the
- * limited precision of floating-point numbers. It accounts for both relative
- * and absolute tolerances to provide a robust comparison method.
+ * This method compares two floating-point numbers, `a` and `b`, considering
+ * the limited precision of floating-point numbers. It accounts for both
+ * relative and absolute tolerances to provide a robust comparison method.
*
* **How it works:** 1. **Calculates absolute difference:** The absolute
* difference between `a` and `b` is calculated. 2. **Determines relative
* tolerance:** The larger of the two absolute values of `a` and `b` is
- * multiplied by a small factor (e.g., 1e-6) to obtain a relative tolerance. 3.
- * **Determines absolute tolerance:** A small fixed value (e.g., `FLT_EPSILON *
- * 8`) is set as the absolute tolerance. 4. **Comparison:** The absolute
- * difference is compared with the larger of the two tolerances. If the
- * difference is smaller, the numbers are considered approximately equal.
+ * multiplied by a small factor (e.g., 1e-6) to obtain a relative tolerance.
+ * 3. **Determines absolute tolerance:** A small fixed value (e.g.,
+ * `FLT_EPSILON * 8`) is set as the absolute tolerance. 4. **Comparison:** The
+ * absolute difference is compared with the larger of the two tolerances. If
+ * the difference is smaller, the numbers are considered approximately equal.
*
* **Why such a method is necessary:** Due to the limited precision of
* floating-point numbers, small rounding errors can occur, causing two
@@ -879,8 +883,8 @@ public static boolean approximately(float a, float b) {
}
/**
- * Clamps the given float value to be between 0 and 1. This method is equivalent
- * to {@link #clamp01(float)}.
+ * Clamps the given float value to be between 0 and 1. This method is
+ * equivalent to {@link #clamp01(float)}.
*
* @param a The value to clamp.
* @return A clamped between 0 and 1.
@@ -922,7 +926,8 @@ public static int closestPowerOfTwo(int value) {
*
* @throws IllegalArgumentException if `from0 == to0` or `from1 == to1`.
*/
- public static float map(float value, float from0, float to0, float from1, float to1) {
+ public static float map(float value, float from0, float to0, float from1,
+ float to1) {
if (from0 == to0 || from1 == to1) {
throw new IllegalArgumentException("Invalid input ranges");
}
diff --git a/src/main/java/mesh/Mesh3D.java b/src/main/java/mesh/Mesh3D.java
index 6ce39e7c..adf62b36 100644
--- a/src/main/java/mesh/Mesh3D.java
+++ b/src/main/java/mesh/Mesh3D.java
@@ -25,8 +25,8 @@ public Mesh3D() {
}
/**
- * Applies the provided {@link IMeshModifier} to this mesh. This is congruent to
- * {@link IMeshModifier#modify(Mesh3D)}.
+ * Applies the provided {@link IMeshModifier} to this mesh. This is congruent
+ * to {@link IMeshModifier#modify(Mesh3D)}.
*
* @param modifier The modifier to apply to this mesh.
* @return this
@@ -82,52 +82,56 @@ public Mesh3D translateZ(float tz) {
}
/**
- * Calculates the axis-aligned bounding box (AABB) for the 3D mesh based on its
- * vertices.
+ * Calculates the axis-aligned bounding box (AABB) for the 3D mesh based on
+ * its vertices.
*
* The bounding box is defined by the minimum and maximum extents of the
- * vertices along the X, Y, and Z axes. If there are no vertices in the mesh, an
- * empty `Bounds3` is returned.
+ * vertices along the X, Y, and Z axes. If there are no vertices in the mesh,
+ * an empty `Bounds3` is returned.
*
*
- * @return A {@link Bounds3} object representing the calculated bounding box of
- * the mesh. The bounding box extends from the minimum vertex coordinate
- * to the maximum vertex coordinate.
+ * @return A {@link Bounds3} object representing the calculated bounding box
+ * of the mesh. The bounding box extends from the minimum vertex
+ * coordinate to the maximum vertex coordinate.
*/
- public Bounds3 calculateBounds() {
- if (vertices.isEmpty())
- return new Bounds3();
-
- Vector3f min = new Vector3f(getVertexAt(0));
- Vector3f max = new Vector3f(getVertexAt(0));
- Bounds3 bounds = new Bounds3();
- for (Vector3f v : vertices) {
- float minX = v.getX() < min.getX() ? v.getX() : min.getX();
- float minY = v.getY() < min.getY() ? v.getY() : min.getY();
- float minZ = v.getZ() < min.getZ() ? v.getZ() : min.getZ();
- float maxX = v.getX() > max.getX() ? v.getX() : max.getX();
- float maxY = v.getY() > max.getY() ? v.getY() : max.getY();
- float maxZ = v.getZ() > max.getZ() ? v.getZ() : max.getZ();
- min.set(minX, minY, minZ);
- max.set(maxX, maxY, maxZ);
- }
- bounds.setMinMax(min, max);
- return bounds;
- }
+ public Bounds3 calculateBounds() {
+ if (vertices.isEmpty())
+ return new Bounds3();
+
+ Vector3f min = new Vector3f(getVertexAt(0));
+ Vector3f max = new Vector3f(getVertexAt(0));
+ Bounds3 bounds = new Bounds3();
+ for (Vector3f v : vertices) {
+ float minX = v.getX() < min.getX() ? v.getX() : min.getX();
+ float minY = v.getY() < min.getY() ? v.getY() : min.getY();
+ float minZ = v.getZ() < min.getZ() ? v.getZ() : min.getZ();
+ float maxX = v.getX() > max.getX() ? v.getX() : max.getX();
+ float maxY = v.getY() > max.getY() ? v.getY() : max.getY();
+ float maxZ = v.getZ() > max.getZ() ? v.getZ() : max.getZ();
+ min.set(minX, minY, minZ);
+ max.set(maxX, maxY, maxZ);
+ }
+ bounds.setMinMax(min, max);
+ return bounds;
+ }
public Vector3f calculateFaceNormal(Face3D face) {
Vector3f faceNormal = new Vector3f();
for (int i = 0; i < face.indices.length; i++) {
Vector3f currentVertex = vertices.get(face.indices[i]);
- Vector3f nextVertex = vertices.get(face.indices[(i + 1) % face.indices.length]);
- float x = (currentVertex.getY() - nextVertex.getY()) * (currentVertex.getZ() + nextVertex.getZ());
- float y = (currentVertex.getZ() - nextVertex.getZ()) * (currentVertex.getX() + nextVertex.getX());
- float z = (currentVertex.getX() - nextVertex.getX()) * (currentVertex.getY() + nextVertex.getY());
+ Vector3f nextVertex = vertices
+ .get(face.indices[(i + 1) % face.indices.length]);
+ float x = (currentVertex.getY() - nextVertex.getY())
+ * (currentVertex.getZ() + nextVertex.getZ());
+ float y = (currentVertex.getZ() - nextVertex.getZ())
+ * (currentVertex.getX() + nextVertex.getX());
+ float z = (currentVertex.getX() - nextVertex.getX())
+ * (currentVertex.getY() + nextVertex.getY());
faceNormal.addLocal(x, y, z);
}
return faceNormal.normalize();
}
-
+
public void removeDoubles(int decimalPlaces) {
for (Vector3f v : vertices)
v.roundLocalDecimalPlaces(decimalPlaces);
diff --git a/src/main/java/mesh/modifier/BevelEdgesModifier.java b/src/main/java/mesh/modifier/BevelEdgesModifier.java
index 18bf63de..b1fbb0ca 100644
--- a/src/main/java/mesh/modifier/BevelEdgesModifier.java
+++ b/src/main/java/mesh/modifier/BevelEdgesModifier.java
@@ -17,11 +17,7 @@ public class BevelEdgesModifier implements IMeshModifier {
public enum WidthType {
- OFFSET,
-
- WIDTH,
-
- DEPTH,
+ OFFSET, WIDTH, DEPTH
}
@@ -37,14 +33,11 @@ public enum WidthType {
private List verticesToAdd;
- private HashMap oldEdgeToNewEdge;
-
- private HashSet processed;
+ private EdgeProcessor edgeProcessor;
public BevelEdgesModifier(float amount) {
setAmount(amount);
- processed = new HashSet<>();
- oldEdgeToNewEdge = new HashMap<>();
+ edgeProcessor = new EdgeProcessor();
verticesToAdd = new ArrayList();
facesToAdd = new ArrayList();
}
@@ -55,81 +48,26 @@ public BevelEdgesModifier() {
@Override
public Mesh3D modify(Mesh3D mesh) {
- if (amount == 0)
+ validateMesh(mesh);
+ if (canExitEarly(mesh)) {
return mesh;
+ }
+
setMesh(mesh);
clearAll();
+
createInsetFaces();
+
createFacesForOldEdges();
createFacesVertex();
+
clearOriginalFaces();
clearOriginalVertices();
+
addNewVertices();
addNewFaces();
- return mesh;
- }
-
- 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);
- List vertices = new ArrayList();
- extracted(face, vertices);
- extracted(vertices);
- mapOldEdgesToNewEdges(face, indices);
- addNewFace(indices);
- }
-
- private void extracted(Face3D face, List vertices) {
- for (int i = 0; i < face.indices.length; i++) {
- Vector3f from = getVertexAt(face, i);
- Vector3f to = getVertexAt(face, i + 1);
-
- float distance = to.distance(from);
- float a = 1 / distance * getAmountByWidthType();
-
- Vector3f v4 = to.subtract(from).mult(a).add(from);
- Vector3f v5 = to.add(to.subtract(from).mult(-a));
- vertices.add(v4);
- vertices.add(v5);
- }
- }
-
- private float getAmountByWidthType() {
- float a;
- switch (widthType) {
- case OFFSET:
- // amount is offset of new edges from original
- a = amount * 2;
- break;
- case WIDTH:
- // amount is width of new faces
- a = inset;
- break;
- case DEPTH:
- a = inset * 2;
- break;
- default:
- // default width type offset
- a = amount * 2;
- break;
- }
- return a;
- }
-
- private void extracted(List 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);
- }
+ return mesh;
}
private void createFacesVertex() {
@@ -139,7 +77,7 @@ private void createFacesVertex() {
Edge3D edge = outgoingEdge;
List indices = new ArrayList();
do {
- Edge3D newEdge = oldEdgeToNewEdge.get(edge);
+ Edge3D newEdge = edgeProcessor.getMappedEdge(edge);
int index = newEdge.fromIndex;
indices.add(index);
edge = helper.getPairNext(edge.fromIndex, edge.toIndex);
@@ -155,33 +93,46 @@ private void createFacesForOldEdges() {
}
private void createFaceForOldEdgeAt(Face3D face, int i) {
- Edge3D edge = getMappedEdge(createEdgeAt(face.indices, i));
- Edge3D pair = getMappedEdge(createEdgeAt(face.indices, i).createPair());
+ Edge3D edge = edgeProcessor
+ .getMappedEdge(edgeProcessor.createEdge(face.indices, i));
+ Edge3D pair = edgeProcessor
+ .getMappedEdge(edgeProcessor.createEdge(face.indices, i).createPair());
- if (isProcessed(edge) || isProcessed(pair))
+ if (edgeProcessor.isProcessed(edge) || edgeProcessor.isProcessed(pair))
return;
- addNewFace(new int[] { edge.toIndex, edge.fromIndex, pair.toIndex,
- pair.fromIndex });
+ createFaceForEdge(edge, pair);
- markAsProcessed(edge);
- markAsProcessed(pair);
+ edgeProcessor.markProcessed(edge);
+ edgeProcessor.markProcessed(pair);
}
- private void mapOldEdgesToNewEdges(Face3D face, int[] indices) {
- for (int i = 0; i < indices.length; i++) {
- Edge3D oldEdge = createEdgeAt(face.indices, i);
- Edge3D newEdge = createEdgeAt(indices, i);
- oldEdgeToNewEdge.put(oldEdge, newEdge);
- }
+ private void createFaceForEdge(Edge3D edge, Edge3D pair) {
+ addNewFace(edge.toIndex, edge.fromIndex, pair.toIndex, pair.fromIndex);
}
- private Edge3D getMappedEdge(Edge3D edge) {
- return oldEdgeToNewEdge.get(edge);
+ private int[] toReverseArray(List values) {
+ Collections.reverse(values);
+ return values.stream().mapToInt(x -> x).toArray();
}
- private Edge3D createEdgeAt(int[] indices, int i) {
- return new Edge3D(indices[i], indices[(i + 1) % indices.length]);
+ 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) {
@@ -191,24 +142,75 @@ private int[] createIndices(int size, int nextVertexIndex) {
return indices;
}
- private int[] toReverseArray(List values) {
- return values.stream().sorted(Collections.reverseOrder()).mapToInt(x -> x)
- .toArray();
+ /**
+ * 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 processFaceEdges(Face3D face) {
+ List 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;
}
- private void clearAll() {
- processed.clear();
- oldEdgeToNewEdge.clear();
- verticesToAdd.clear();
- facesToAdd.clear();
+ /**
+ * Creates the inset vertices from the processed edge vertices.
+ *
+ * @param vertices the processed edge vertices.
+ */
+ private void createInsetVertices(List 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);
+ }
}
- private void markAsProcessed(Edge3D edge) {
- processed.add(edge);
+ /**
+ * 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 boolean isProcessed(Edge3D edge) {
- return processed.contains(edge);
+ 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) {
@@ -231,7 +233,7 @@ private void clearOriginalFaces() {
mesh.faces.clear();
}
- private void addNewFace(int[] indices) {
+ private void addNewFace(int... indices) {
facesToAdd.add(new Face3D(indices));
}
@@ -256,4 +258,46 @@ public void setWidthType(WidthType widthType) {
this.widthType = widthType;
}
+ private class EdgeProcessor {
+
+ private HashSet processed;
+
+ private HashMap edgeMapping;
+
+ public EdgeProcessor() {
+ processed = new HashSet();
+ edgeMapping = new HashMap();
+ }
+
+ 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();
+ }
+
+ }
+
}
diff --git a/src/main/java/mesh/modifier/ExtrudeModifier.java b/src/main/java/mesh/modifier/ExtrudeModifier.java
index 7a3a7bee..d75cd463 100644
--- a/src/main/java/mesh/modifier/ExtrudeModifier.java
+++ b/src/main/java/mesh/modifier/ExtrudeModifier.java
@@ -26,14 +26,36 @@
*/
public class ExtrudeModifier implements IMeshModifier, FaceModifier {
+ /**
+ * The default scaling factor applied during extrusion. Defaults to 1.0, which
+ * means no scaling.
+ */
private static final float DEFAULT_SCALE = 1.0f;
+ /**
+ * The default extrusion amount applied to faces. Defaults to 0.0, meaning
+ * extrusion with a dinstance of zero.
+ */
private static final float DEFAULT_AMOUNT = 0.0f;
+ /**
+ * Flag indicating whether the original faces should be removed after
+ * extrusion. If true, the original faces are removed; if false, they remain
+ * in the mesh.
+ */
private boolean removeFaces;
+ /**
+ * The scaling factor applied to the extruded geometry. A value of 1.0
+ * maintains the original size of the faces, while values greater or less than
+ * 1.0 scale the extruded faces proportionally.
+ */
private float scale;
+ /**
+ * The distance to extrude faces by. Positive values extrude outward along the
+ * normal of the face, while negative values extrude inward.
+ */
private float amount;
/**
@@ -161,8 +183,12 @@ private void extrudeFace(Mesh3D mesh, Face3D face) {
Vector3f center = mesh.calculateFaceCenter(face);
for (int i = 0; i < n; i++) {
- Vector3f vertex = mesh.vertices.get(face.indices[i]).subtract(center)
- .mult(scale).add(center).add(normal.mult(amount));
+ Vector3f vertex = mesh.vertices.get(
+ face.indices[i])
+ .subtract(center)
+ .mult(scale)
+ .add(center)
+ .add(normal.mult(amount));
mesh.add(vertex);
mesh.addFace(face.indices[i], face.indices[(i + 1) % n],
nextIndex + ((i + 1) % n), nextIndex + i);
diff --git a/src/main/java/mesh/modifier/HolesModifier.java b/src/main/java/mesh/modifier/HolesModifier.java
index 3edc1b40..51d60431 100644
--- a/src/main/java/mesh/modifier/HolesModifier.java
+++ b/src/main/java/mesh/modifier/HolesModifier.java
@@ -1,27 +1,73 @@
package mesh.modifier;
+import java.util.ArrayList;
import java.util.Collection;
import mesh.Face3D;
import mesh.Mesh3D;
+/**
+ * The {@code HolesModifier} class modifies a 3D mesh by creating holes in its
+ * faces. This is achieved by insetting specified faces to a percentage of their
+ * original size, effectively leaving holes where the faces used to be.
+ *
+ *
+ *
+ * The behavior is defined as follows:
+ * - A `holePercentage` of 0.0 leaves the face unchanged (no hole).
+ * - A `holePercentage` of 1.0 removes the face entirely
+ * (hole consumes the full face area).
+ * - Values between 0.0 and 1.0 create proportionally smaller holes,
+ * with the face scaled to (1 - holePercentage).
+ *
+ * Key features:
+ * - Adjusts faces to create holes based on a specified percentage.
+ * - Can work with the entire mesh, specific collections of faces, or a
+ * single face.
+ *
+ */
public class HolesModifier implements IMeshModifier, FaceModifier {
+ /**
+ * The percentage of the face's size to retain after creating the hole. A
+ * value of 0.0 leaves the face unchanged, while hole consumes the full face
+ * area with a value of 1.0.
+ */
private float holePercentage;
+ /**
+ * Default constructor initializes the modifier with a hole percentage of 0.5
+ * (50%).
+ */
public HolesModifier() {
this.holePercentage = 0.5f;
}
+ /**
+ * Constructs the {@code HolesModifier} with a specified hole percentage.
+ *
+ * @param holePercentage The percentage of the face's size to retain after
+ * creating the hole. Must be between 0.0 and 1.0,
+ * inclusive.
+ * @throws IllegalArgumentException if the percentage is outside the valid
+ * range.
+ */
public HolesModifier(float holePercentage) {
- this.holePercentage = holePercentage;
+ setHolePercentage(holePercentage);
}
+ /**
+ * Modifies the given mesh to create holes by insetting it's faces to a
+ * specified percentage of their original size.
+ *
+ * @see #HolesModifier()
+ * @param mesh The mesh to modify.
+ * @return The modified mesh.
+ * @throws IllegalArgumentException if the mesh is null.
+ */
@Override
public Mesh3D modify(Mesh3D mesh) {
- if (mesh == null) {
- throw new IllegalArgumentException("Mesh cannot be null.");
- }
+ validateMesh(mesh);
return modify(mesh, mesh.getFaces());
}
@@ -29,15 +75,7 @@ public Mesh3D modify(Mesh3D mesh) {
* Modifies the given mesh to create holes by insetting the specified faces to
* a specified percentage of their original size.
*
- *
- * The behavior is defined as follows:
- * - A `holePercentage` of 0.0 leaves the face unchanged (no hole).
- * - A `holePercentage` of 1.0 removes the face entirely
- * (hole consumes the full face area).
- * - Values between 0.0 and 1.0 create proportionally smaller holes,
- * with the face scaled to (1 - holePercentage).
- *
- *
+ * @see #HolesModifier()
* @param mesh The mesh to modify.
* @param faces The faces to be inset.
* @return The modified mesh.
@@ -45,31 +83,47 @@ public Mesh3D modify(Mesh3D mesh) {
*/
@Override
public Mesh3D modify(Mesh3D mesh, Collection faces) {
- if (mesh == null) {
- throw new IllegalArgumentException("Mesh cannot be null.");
- }
- if (faces == null) {
- throw new IllegalArgumentException("Faces cannot be null.");
- }
- if (mesh.faces.isEmpty() || holePercentage == 0) {
+ validateMesh(mesh);
+ validateFaces(faces);
+ if (canExitEarly(mesh)) {
return mesh;
}
- createExtrudeModifier().modify(mesh, faces);
+ Collection facesToModify = faces;
+ if (faces == mesh.faces) {
+ facesToModify = new ArrayList(mesh.faces);
+ }
+ createExtrudeModifier().modify(mesh, facesToModify);
return mesh;
}
+ /**
+ * Modifies the given mesh to create holes by insetting the specified face to
+ * a specified percentage of it's original size.
+ *
+ * @see #HolesModifier()
+ * @param mesh The mesh to modify.
+ * @param face The face to be inset.
+ * @return The modified mesh.
+ * @throws IllegalArgumentException if the mesh or face are null.
+ */
@Override
public Mesh3D modify(Mesh3D mesh, Face3D face) {
- if (mesh == null) {
- throw new IllegalArgumentException("Mesh cannot be null.");
- }
- if (face == null) {
- throw new IllegalArgumentException("Face cannot be null.");
+ validateMesh(mesh);
+ validateFace(face);
+ if (canExitEarly(mesh)) {
+ return mesh;
}
createExtrudeModifier().modify(mesh, face);
return mesh;
}
+ /**
+ * Creates an instance of {@code ExtrudeModifier} configured to create holes.
+ * The modifier will scale faces based on the specified hole percentage, with
+ * no extrusion applied, and will remove the original faces.
+ *
+ * @return A configured {@code ExtrudeModifier} instance.
+ */
private ExtrudeModifier createExtrudeModifier() {
ExtrudeModifier modifier = new ExtrudeModifier();
modifier.setScale(holePercentage);
@@ -78,14 +132,76 @@ private ExtrudeModifier createExtrudeModifier() {
return modifier;
}
+ /**
+ * Determines whether the modification can exit early based on the current
+ * mesh state and hole percentage.
+ *
+ * @param mesh The mesh to evaluate for early exit.
+ * @return {@code true} if the modification can exit early (e.g., the mesh has
+ * no faces or the hole percentage is zero), {@code false} otherwise.
+ */
+ private boolean canExitEarly(Mesh3D mesh) {
+ return mesh.faces.isEmpty() || holePercentage == 0;
+ }
+
+ /**
+ * Validates that the face is not null.
+ *
+ * @param face the face to validate.
+ * @throws IllegalArgumentException if the face is null.
+ */
+ private void validateFace(Face3D face) {
+ if (face == null) {
+ throw new IllegalArgumentException("Face cannot be null.");
+ }
+ }
+
+ /**
+ * Validates that the mesh is not null.
+ *
+ * @param mesh the mesh to validate.
+ * @throws IllegalArgumentException if the mesh is null.
+ */
+ private void validateMesh(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ }
+
+ /**
+ * Validates that the faces collection is not null.
+ *
+ * @param faces the collection of faces to validate.
+ * @throws IllegalArgumentException if the collection is null.
+ */
+ private void validateFaces(Collection faces) {
+ if (faces == null) {
+ throw new IllegalArgumentException("Faces cannot be null.");
+ }
+ }
+
+ /**
+ * Gets the current percentage of the face's size retained after creating the
+ * hole.
+ *
+ * @return The hole percentage.
+ */
public float getHolePercentage() {
return holePercentage;
}
+ /**
+ * Sets the percentage of the face's size to retain after creating the hole.
+ *
+ * @param holePercentage The new hole percentage to set. Must be between 0.0
+ * and 1.0, inclusive.
+ * @throws IllegalArgumentException if the percentage is outside the valid
+ * range.
+ */
public void setHolePercentage(float holePercentage) {
if (holePercentage < 0 || holePercentage > 1) {
throw new IllegalArgumentException(
- "Hole percentage must be between 0 and 1.");
+ "Hole percentage must be between 0 and 1 inclusive.");
}
this.holePercentage = holePercentage;
}
diff --git a/src/main/java/mesh/modifier/InsetModifier.java b/src/main/java/mesh/modifier/InsetModifier.java
index a77d205c..5e69bcd0 100644
--- a/src/main/java/mesh/modifier/InsetModifier.java
+++ b/src/main/java/mesh/modifier/InsetModifier.java
@@ -10,65 +10,143 @@
import mesh.Mesh3D;
/**
- * Renamed process face to inset face
+ * The {@code InsetModifier} modifies a mesh by applying an inset operation to
+ * its faces. Insetting creates smaller, inset faces inside the original faces,
+ * producing a beveled or framed effect. This modifier supports applying the
+ * inset operation to all faces in a mesh, a specific collection of faces, or a
+ * single face.
+ *
+ * The inset factor determines how far the vertices of the new face are moved
+ * inward, based on the edges' lengths of the original face.
+ *
*/
public class InsetModifier implements IMeshModifier, FaceModifier {
+ /**
+ * The default inset factor applied if no custom value is specified. This
+ * value determines the default distance vertices are moved inward when the
+ * inset operation is performed.
+ */
private static final float DEFAULT_INSET = 0.1f;
+ /**
+ * The index for the next available vertex in the mesh. This value is used to
+ * keep track of where to insert new vertices during the inset operation.
+ */
private int nextIndex;
+ /**
+ * The inset factor that controls the distance vertices are moved inward
+ * during the inset operation. A higher value results in a deeper inset, while
+ * a smaller value results in a shallower inset.
+ */
private float inset;
+ /**
+ * The mesh being modified by the {@code InsetModifier}. This field is set
+ * during the modification process and stores the reference to the mesh that
+ * is being operated upon.
+ */
private Mesh3D mesh;
+ /**
+ * Creates an {@code InsetModifier} with the default inset factor (0.1).
+ */
public InsetModifier() {
this(DEFAULT_INSET);
}
+ /**
+ * Creates an {@code InsetModifier} with the specified inset factor.
+ *
+ * @param inset the inset factor, controlling the distance vertices are moved
+ * inward.
+ */
public InsetModifier(float inset) {
this.inset = inset;
}
+ /**
+ * Modifies the entire mesh by applying the inset operation to all its faces.
+ *
+ * @param mesh the {@code Mesh3D} to modify.
+ * @return the modified mesh.
+ * @throws IllegalArgumentException if the mesh is null.
+ */
@Override
public Mesh3D modify(Mesh3D mesh) {
- if (mesh == null) {
- throw new IllegalArgumentException("Mesh cannot be null.");
+ validateMesh(mesh);
+ if (mesh.faces.isEmpty()) {
+ return mesh;
}
modify(mesh, mesh.getFaces());
return mesh;
}
+ /**
+ * Modifies the specified collection of faces in the mesh by applying the
+ * inset operation.
+ *
+ * @param mesh the {@code Mesh3D} to modify.
+ * @param faces the collection of {@code Face3D} instances to modify.
+ * @return the modified mesh.
+ * @throws IllegalArgumentException if the mesh or faces are null.
+ */
@Override
public Mesh3D modify(Mesh3D mesh, Collection faces) {
- if (mesh == null) {
- throw new IllegalArgumentException("Mesh cannot be null.");
- }
- if (faces == null) {
- throw new IllegalArgumentException("Faces cannot be null.");
+ validateMesh(mesh);
+ validateFaces(faces);
+ Collection facesToModify = faces;
+ if (faces == mesh.faces) {
+ facesToModify = new ArrayList(mesh.faces);
}
setMesh(mesh);
- for (Face3D face : faces) {
+ for (Face3D face : facesToModify) {
insetFace(face);
}
return mesh;
}
+ /**
+ * Modifies a single face in the mesh by applying the inset operation.
+ *
+ * @param mesh the {@code Mesh3D} containing the face.
+ * @param face the {@code Face3D} to modify.
+ * @return the modified mesh.
+ * @throws IllegalArgumentException if the mesh or face is null.
+ */
@Override
public Mesh3D modify(Mesh3D mesh, Face3D face) {
- if (mesh == null) {
- throw new IllegalArgumentException("Mesh cannot be null.");
- }
- if (face == null) {
- throw new IllegalArgumentException("Face cannot be null.");
- }
+ validateMesh(mesh);
+ validateFace(face);
setMesh(mesh);
insetFace(face);
return mesh;
}
+ /**
+ * Applies the inset operation to a single face, creating inset vertices and
+ * updating the face structure.
+ *
+ * @param face the face to modify.
+ */
+ private void insetFace(Face3D face) {
+ updateNextIndex();
+ createInsetVertices(processFaceEdges(face));
+ for (int i = 0; i < face.getVertexCount(); i++) {
+ createFaceAt(face, i);
+ }
+ replaceOriginalFaceWithInsetFace(face);
+ }
+
+ /**
+ * 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 processFaceEdges(Face3D face) {
- List verts = new ArrayList<>();
+ List vertices = new ArrayList<>();
for (int i = 0; i < face.indices.length; i++) {
int index0 = face.indices[i];
int index1 = face.indices[(i + 1) % face.indices.length];
@@ -82,27 +160,17 @@ private List processFaceEdges(Face3D face) {
Vector3f v4 = v1.subtract(v0).mult(insetFactor).add(v0);
Vector3f v5 = v1.add(v1.subtract(v0).mult(-insetFactor));
- verts.add(v4);
- verts.add(v5);
- }
- return verts;
- }
-
- private void insetFace(Face3D face) {
- updateNextIndex();
- createInsetVertices(processFaceEdges(face));
- for (int i = 0; i < face.getVertexCount(); i++) {
- createFaceAt(face, i);
- }
- replaceOriginalFaceWithInsetFace(face);
- }
-
- private void replaceOriginalFaceWithInsetFace(Face3D face) {
- for (int i = 0; i < face.getVertexCount(); i++) {
- face.indices[i] = nextIndex + i;
+ 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 vertices) {
for (int i = 1; i < vertices.size(); i += 2) {
int a = vertices.size() - 2 + i;
@@ -113,6 +181,24 @@ private void createInsetVertices(List vertices) {
}
}
+ /**
+ * Replaces the original face with the inset face.
+ *
+ * @param face the face to replace.
+ */
+ private void replaceOriginalFaceWithInsetFace(Face3D face) {
+ for (int i = 0; i < face.getVertexCount(); i++) {
+ face.indices[i] = nextIndex + i;
+ }
+ }
+
+ /**
+ * Creates a new face at the specified index using the original and inset
+ * vertices.
+ *
+ * @param face the original face.
+ * @param i the index of the vertex to process.
+ */
private void createFaceAt(Face3D face, int i) {
int n = face.indices.length;
int index0 = face.indices[i];
@@ -122,22 +208,82 @@ private void createFaceAt(Face3D face, int i) {
mesh.addFace(index0, index1, index2, index3);
}
+ /**
+ * Validates that the faces collection is not null.
+ *
+ * @param faces the collection of faces to validate.
+ * @throws IllegalArgumentException if the collection is null.
+ */
+ private void validateFaces(Collection faces) {
+ if (faces == null) {
+ throw new IllegalArgumentException("Faces cannot be null.");
+ }
+ }
+
+ /**
+ * Validates that the mesh is not null.
+ *
+ * @param mesh the mesh to validate.
+ * @throws IllegalArgumentException if the mesh is null.
+ */
+ private void validateMesh(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ }
+
+ /**
+ * Validates that the face is not null.
+ *
+ * @param face the face to validate.
+ * @throws IllegalArgumentException if the face is null.
+ */
+ private void validateFace(Face3D face) {
+ if (face == null) {
+ throw new IllegalArgumentException("Face cannot be null.");
+ }
+ }
+
+ /**
+ * Calculates the inset factor based on the edge length.
+ *
+ * @param edgeLength the length of the edge.
+ * @return the inset factor.
+ */
private float calculateInsetFactor(float edgeLength) {
return edgeLength > 0 ? (1f / edgeLength) * inset : 0f;
}
+ /**
+ * Updates the next available vertex index.
+ */
private void updateNextIndex() {
nextIndex = mesh.vertices.size();
}
+ /**
+ * Sets the mesh to be modified.
+ *
+ * @param mesh the mesh to set.
+ */
private void setMesh(Mesh3D mesh) {
this.mesh = mesh;
}
+ /**
+ * Retrieves the inset factor.
+ *
+ * @return the inset factor.
+ */
public float getInset() {
return inset;
}
+ /**
+ * Sets the inset factor.
+ *
+ * @param inset the inset factor to set.
+ */
public void setInset(float inset) {
this.inset = inset;
}
diff --git a/src/main/java/mesh/modifier/PseudoWireframeModifier.java b/src/main/java/mesh/modifier/PseudoWireframeModifier.java
index 89893ad1..86101538 100644
--- a/src/main/java/mesh/modifier/PseudoWireframeModifier.java
+++ b/src/main/java/mesh/modifier/PseudoWireframeModifier.java
@@ -16,14 +16,34 @@
*/
public class PseudoWireframeModifier implements IMeshModifier {
+ /**
+ * Default hole percentage used for the pseudo-wireframe effect. A value of
+ * 0.9 means 90% of the face will be converted into a hole.
+ */
private static final float DEFAULT_HOLE_PECENTAGE = 0.9f;
+ /**
+ * Default thickness used for solidifying the mesh. A value of 0.02 defines
+ * the thickness of the solidified areas.
+ */
private static final float DEFAULT_THICKNESS = 0.02f;
+ /**
+ * The percentage of the face area to be converted into holes. Must be a value
+ * between 0 and 1.
+ */
private float holePercentage;
+ /**
+ * The thickness of the solidified mesh after the hole-creation step. Must be
+ * greater than zero.
+ */
private float thickness;
+ /**
+ * The current 3D mesh being modified. This is set during the modification
+ * process.
+ */
private Mesh3D mesh;
/**
diff --git a/src/main/java/mesh/modifier/RandomHolesModifier.java b/src/main/java/mesh/modifier/RandomHolesModifier.java
index 9340ac2c..5fbd1cef 100644
--- a/src/main/java/mesh/modifier/RandomHolesModifier.java
+++ b/src/main/java/mesh/modifier/RandomHolesModifier.java
@@ -7,89 +7,287 @@
import mesh.Face3D;
import mesh.Mesh3D;
+/**
+ * A mesh modifier that creates random holes in the given 3D mesh by extruding
+ * and removing specified faces. The size of the holes is determined randomly as
+ * a percentage of the original face size within a defined range.
+ *
+ * This modifier supports modifying all faces, a single face, or a subset of
+ * faces in a 3D mesh.
+ *
+ */
public class RandomHolesModifier implements IMeshModifier, FaceModifier {
+ /**
+ * The default minimum amount for the hole size as a percentage of the face
+ * area.
+ */
+ private static final float DEFAULT_MIN_AMOUNT = 0.1f;
+
+ /**
+ * The default maximum amount for the hole size as a percentage of the face
+ * area.
+ */
+ private static final float DEFAULT_MAX_AMOUNT = 0.9f;
+
+ /**
+ * The minimum amount for the hole size as a percentage of the face area.
+ */
private float minAmount;
+ /**
+ * The maximum amount for the hole size as a percentage of the face area.
+ */
private float maxAmount;
+ /**
+ * The seed for the random number generator used to determine hole sizes.
+ */
private long seed;
+ /**
+ * A random number generator used to calculate random hole sizes.
+ */
private Random random;
+ /**
+ * An {@link ExtrudeModifier} used to create the holes by extruding and
+ * removing faces.
+ */
private ExtrudeModifier modifier;
+ /**
+ * Creates a new RandomHolesModifier with the default minimum and maximum hole
+ * percentages.
+ *
+ * Default values:
+ *
+ * - Minimum hole percentage: 10% (0.1)
+ * - Maximum hole percentage: 90% (0.9)
+ *
+ *
+ */
public RandomHolesModifier() {
- this(0.1f, 0.9f);
+ this(DEFAULT_MIN_AMOUNT, DEFAULT_MAX_AMOUNT);
+ validateAmountRange(DEFAULT_MIN_AMOUNT, DEFAULT_MAX_AMOUNT);
}
+ /**
+ * Creates a new RandomHolesModifier with specified minimum and maximum hole
+ * percentages.
+ *
+ * @param minAmount the minimum size of a hole as a percentage of the original
+ * face size. Must be in the range [0, 1].
+ * @param maxAmount the maximum size of a hole as a percentage of the original
+ * face size. Must be in the range [0, 1] and greater than or
+ * equal to {@code minAmount}.
+ * @throws IllegalArgumentException if {@code minAmount} or {@code maxAmount}
+ * are out of range or if
+ * {@code minAmount > maxAmount}.
+ */
public RandomHolesModifier(float minAmount, float maxAmount) {
+ validateAmountRange(minAmount, maxAmount);
this.minAmount = minAmount;
this.maxAmount = maxAmount;
- this.random = new Random(seed);
+ this.random = new Random();
this.modifier = new ExtrudeModifier();
}
+ /**
+ * Modifies the entire mesh by creating random holes in all its faces.
+ *
+ * @param mesh the mesh to modify.
+ * @return the modified mesh.
+ * @throws IllegalArgumentException if the mesh is {@code null}.
+ */
@Override
public Mesh3D modify(Mesh3D mesh) {
+ validateMesh(mesh);
return modify(mesh, new ArrayList(mesh.faces));
}
+ /**
+ * Modifies a single face in the mesh by creating a random hole.
+ *
+ * @param mesh the mesh containing the face.
+ * @param face the face to modify.
+ * @return the modified mesh.
+ * @throws IllegalArgumentException if the mesh or face is {@code null}.
+ */
@Override
public Mesh3D modify(Mesh3D mesh, Face3D face) {
- if (mesh == null || face == null)
- throw new IllegalArgumentException();
-
+ validateMesh(mesh);
+ validateFace(face);
makeHole(mesh, face);
- mesh.removeFace(face);
return mesh;
}
+ /**
+ * Modifies a collection of faces in the mesh by creating random holes.
+ *
+ * @param mesh the mesh containing the faces.
+ * @param faces the faces to modify.
+ * @return the modified mesh.
+ * @throws IllegalArgumentException if the mesh or faces are {@code null}.
+ */
@Override
public Mesh3D modify(Mesh3D mesh, Collection faces) {
- if (mesh == null || faces == null)
- throw new IllegalArgumentException();
-
- for (Face3D face : faces)
+ validateMesh(mesh);
+ validateFaces(faces);
+ Collection facesToModify = faces;
+ if (faces == mesh.faces) {
+ facesToModify = new ArrayList(mesh.faces);
+ }
+ for (Face3D face : facesToModify) {
makeHole(mesh, face);
-
- mesh.faces.removeAll(faces);
+ }
return mesh;
}
+ /**
+ * Creates a hole by extruding (inset) and removing the specified face.
+ *
+ * @param mesh the mesh containing the face.
+ * @param face the face to modify.
+ */
private void makeHole(Mesh3D mesh, Face3D face) {
float amount = createRandomAmount();
modifier.setScale(amount);
+ modifier.setRemoveFaces(true);
modifier.modify(mesh, face);
}
+ /**
+ * Generates a random hole percentage within the range defined by
+ * {@code minAmount} and {@code maxAmount}.
+ *
+ * @return a random hole percentage.
+ */
private float createRandomAmount() {
return minAmount + random.nextFloat() * (maxAmount - minAmount);
}
+ /**
+ * Validates that the given mesh is not null.
+ *
+ * @param mesh the {@link Mesh3D} instance to validate.
+ * @throws IllegalArgumentException if {@code mesh} is null.
+ */
+ private void validateMesh(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ }
+
+ /**
+ * Validates that the given collection of faces is not null.
+ *
+ * @param faces a collection of {@link Face3D} instances to validate.
+ * @throws IllegalArgumentException if {@code faces} is null.
+ */
+ private void validateFaces(Collection faces) {
+ if (faces == null) {
+ throw new IllegalArgumentException("Faces cannot be null.");
+ }
+ }
+
+ /**
+ * Validates that the given face is not null.
+ *
+ * @param face the {@link Face3D} instance to validate.
+ * @throws IllegalArgumentException if {@code face} is null.
+ */
+ private void validateFace(Face3D face) {
+ if (face == null) {
+ throw new IllegalArgumentException("Face cannot be null.");
+ }
+ }
+
+ /**
+ * Gets the minimum hole percentage.
+ *
+ * @return the minimum hole percentage.
+ */
public float getMinAmount() {
return minAmount;
}
+ /**
+ * Sets the minimum hole percentage.
+ *
+ * @param minAmount the minimum hole percentage. Must be in the range [0, 1].
+ * @throws IllegalArgumentException if {@code minAmount} is out of range or
+ * greater than the current
+ * {@code maxAmount}.
+ */
public void setMinAmount(float minAmount) {
+ validateAmountRange(minAmount, maxAmount);
this.minAmount = minAmount;
}
+ /**
+ * Gets the maximum hole percentage.
+ *
+ * @return the maximum hole percentage.
+ */
public float getMaxAmount() {
return maxAmount;
}
+ /**
+ * Sets the maximum hole percentage.
+ *
+ * @param maxAmount the maximum hole percentage. Must be in the range [0, 1].
+ * @throws IllegalArgumentException if {@code maxAmount} is out of range or
+ * less than the current {@code minAmount}.
+ */
public void setMaxAmount(float maxAmount) {
+ validateAmountRange(minAmount, maxAmount);
this.maxAmount = maxAmount;
}
+ /**
+ * Validates that the given range of amounts is within acceptable bounds.
+ *
+ * The method checks that the {@code minAmount} and {@code maxAmount} values
+ * satisfy the following conditions:
+ *
+ * - {@code minAmount} is greater than or equal to 0.
+ * - {@code maxAmount} is less than or equal to 1.
+ * - {@code minAmount} is less than or equal to {@code maxAmount}.
+ *
+ *
+ *
+ * @param minAmount the minimum hole percentage to validate.
+ * @param maxAmount the maximum hole percentage to validate.
+ * @throws IllegalArgumentException if the range does not satisfy the
+ * conditions:
+ * {@code 0 <= minAmount <= maxAmount <= 1}.
+ */
+ private void validateAmountRange(float minAmount, float maxAmount) {
+ if (minAmount < 0 || maxAmount > 1 || minAmount > maxAmount) {
+ throw new IllegalArgumentException(
+ "Amounts must satisfy: 0 <= minAmount <= maxAmount <= 1.");
+ }
+ }
+
+ /**
+ * Gets the current random seed.
+ *
+ * @return the random seed.
+ */
public long getSeed() {
return seed;
}
+ /**
+ * Sets a new random seed.
+ *
+ * @param seed the new seed value.
+ */
public void setSeed(long seed) {
this.seed = seed;
- random = new Random(seed);
+ this.random = new Random(seed);
}
}
diff --git a/src/main/java/mesh/modifier/ShearModifier.java b/src/main/java/mesh/modifier/ShearModifier.java
new file mode 100644
index 00000000..7cefd734
--- /dev/null
+++ b/src/main/java/mesh/modifier/ShearModifier.java
@@ -0,0 +1,159 @@
+package mesh.modifier;
+
+import math.Vector3f;
+import mesh.Mesh3D;
+
+/**
+ * A modifier that applies a shear transformation to a 3D mesh. The shear effect
+ * distorts the mesh along a specified axis, creating a slanted or skewed
+ * appearance.
+ *
+ * The transformation is applied to each vertex of the mesh based on the
+ * specified shear axis and factor.
+ */
+public class ShearModifier implements IMeshModifier {
+
+ /**
+ * Represents the axis and plane along which the shear is applied.
+ *
+ *
+ * - XY: Shear along the X-axis based on the Y-coordinate.
+ * - XZ: Shear along the X-axis based on the Z-coordinate.
+ * - YX: Shear along the Y-axis based on the X-coordinate.
+ * - YZ: Shear along the Y-axis based on the Z-coordinate.
+ * - ZX: Shear along the Z-axis based on the X-coordinate.
+ * - ZY: Shear along the Z-axis based on the Y-coordinate.
+ *
+ */
+ public enum ShearAxis {
+ XY, XZ, YX, YZ, ZX, ZY
+ }
+
+ /**
+ * The factor by which the mesh is sheared.
+ */
+ private float shearFactor;
+
+ /**
+ * The axis along which the shear transformation is applied.
+ */
+ private ShearAxis axis;
+
+ /**
+ * Constructs a ShearModifier with the specified shear axis and factor.
+ *
+ * @param axis the axis along which the shear transformation is applied
+ * @param shearFactor the factor by which the mesh is sheared
+ * @throws IllegalArgumentException if axis is null
+ */
+ public ShearModifier(ShearAxis axis, float shearFactor) {
+ if (axis == null) {
+ throw new IllegalArgumentException("Shear axis cannot be null.");
+ }
+ this.axis = axis;
+ this.shearFactor = shearFactor;
+ }
+
+ /**
+ * Applies the shear transformation to the given mesh.
+ *
+ * @param mesh the mesh to be modified
+ * @return the modified mesh with the shear transformation applied
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ validateMesh(mesh);
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ applyShear(mesh);
+ return mesh;
+ }
+
+ /**
+ * Applies the shear transformation to all vertices in the given mesh.
+ *
+ * This method uses a parallel stream to process the vertices, applying the
+ * shear transformation in a concurrent manner for improved performance on
+ * large meshes. The shear transformation is determined by the specified shear
+ * axis and factor.
+ *
+ * @param mesh the mesh whose vertices will be sheared
+ * @throws IllegalArgumentException if the mesh is null or contains null
+ * vertices
+ */
+ private void applyShear(Mesh3D mesh) {
+ mesh.vertices.parallelStream().forEach(this::applyShearToVertex);
+ }
+
+ /**
+ * Applies the shear transformation to a single vertex based on the specified
+ * shear axis and factor.
+ *
+ * @param vertex the vertex to modify
+ */
+ private void applyShearToVertex(Vector3f vertex) {
+ switch (axis) {
+ case XY -> vertex.x += shearFactor * vertex.y;
+ case XZ -> vertex.x += shearFactor * vertex.z;
+ case YX -> vertex.y += shearFactor * vertex.x;
+ case YZ -> vertex.y += shearFactor * vertex.z;
+ case ZX -> vertex.z += shearFactor * vertex.x;
+ case ZY -> vertex.z += shearFactor * vertex.y;
+ default ->
+ throw new IllegalArgumentException("Unsupported shear axis: " + axis);
+ }
+ }
+
+ /**
+ * Validates that the mesh is not null.
+ *
+ * @param mesh the mesh to validate.
+ * @throws IllegalArgumentException if the mesh is null.
+ */
+ private void validateMesh(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ }
+
+ /**
+ * Gets the current shear axis.
+ *
+ * @return the shear axis
+ */
+ public ShearAxis getAxis() {
+ return axis;
+ }
+
+ /**
+ * Sets the shear axis.
+ *
+ * @param axis the shear axis to set
+ */
+ public void setAxis(ShearAxis axis) {
+ if (axis == null) {
+ throw new IllegalArgumentException("Shear axis cannot be null.");
+ }
+ this.axis = axis;
+ }
+
+ /**
+ * Gets the current shear factor.
+ *
+ * @return the shear factor
+ */
+ public float getShearFactor() {
+ return shearFactor;
+ }
+
+ /**
+ * Sets the shear factor.
+ *
+ * @param shearFactor the shear factor to set
+ */
+ public void setShearFactor(float shearFactor) {
+ this.shearFactor = shearFactor;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/mesh/modifier/SnapToGroundModifier.java b/src/main/java/mesh/modifier/SnapToGroundModifier.java
index 847b6944..f735f288 100644
--- a/src/main/java/mesh/modifier/SnapToGroundModifier.java
+++ b/src/main/java/mesh/modifier/SnapToGroundModifier.java
@@ -37,10 +37,19 @@ public class SnapToGroundModifier implements IMeshModifier {
/** The mesh to be modified. */
private Mesh3D mesh;
+ /**
+ * Constructs a new SnapToGroundModifier with a default ground level of 0.
+ */
public SnapToGroundModifier() {
this(0);
}
+ /**
+ * Constructs a new SnapToGroundModifier with a specified ground level.
+ *
+ * @param groundLevel the vertical level to which the mesh's highest point
+ * should be snapped.
+ */
public SnapToGroundModifier(float groundLevel) {
this.groundLevel = groundLevel;
}
diff --git a/src/main/java/mesh/modifier/SolidifyModifier.java b/src/main/java/mesh/modifier/SolidifyModifier.java
index 77d4696c..a7956940 100644
--- a/src/main/java/mesh/modifier/SolidifyModifier.java
+++ b/src/main/java/mesh/modifier/SolidifyModifier.java
@@ -11,28 +11,68 @@
import mesh.util.FaceBridging;
import mesh.util.VertexNormals;
+/**
+ * A modifier that solidifies a 3D mesh by creating an inner mesh offset along
+ * vertex normals, and bridging the edges between the original and inner meshes.
+ *
+ * This modifier is commonly used in modeling workflows to add thickness to 2D
+ * surfaces or thin meshes.
+ *
+ *
+ *
+ * Workflow:
+ * 1. Creates a copy of the input mesh as the inner mesh.
+ * 2. Offsets the inner mesh vertices along their normals by the
+ * specified thickness.
+ * 3. Reverses the face directions of the inner mesh to ensure proper normals.
+ * 4. Bridges the edges between the original and inner mesh to create a
+ * closed solid.
+ *
+ */
public class SolidifyModifier implements IMeshModifier {
+ /** The thickness to apply when solidifying the mesh. */
private float thickness;
+ /** The original mesh to modify. */
private Mesh3D mesh;
+ /** The inner mesh created by offsetting the original mesh. */
private Mesh3D innerMesh;
+ /** The vertex normals of the original mesh. */
private List vertexNormals;
+ /** The edges of the original mesh. */
private HashSet edges;
+ /** The faces of the original mesh before modifications. */
private List originalFaces;
+ /**
+ * Creates a new SolidifyModifier with the default thickness of 0.01.
+ */
public SolidifyModifier() {
this(0.01f);
}
+ /**
+ * Creates a new SolidifyModifier with the specified thickness.
+ *
+ * @param thickness The thickness to apply when solidifying the mesh.
+ * @throws IllegalArgumentException If the thickness is negative.
+ */
public SolidifyModifier(float thickness) {
this.thickness = thickness;
}
+ /**
+ * Modifies the given mesh by solidifying it.
+ *
+ * @param mesh The mesh to modify.
+ * @return The solidified mesh.
+ * @throws IllegalArgumentException If the mesh is null.
+ */
@Override
public Mesh3D modify(Mesh3D mesh) {
setMesh(mesh);
@@ -50,6 +90,10 @@ public Mesh3D modify(Mesh3D mesh) {
return mesh;
}
+ /**
+ * Creates the inner mesh by copying the original mesh, flipping its faces,
+ * and offsetting its vertices along their normals.
+ */
private void createInnerMesh() {
initializeInnerMesh();
appendInnerMesh();
@@ -57,16 +101,26 @@ private void createInnerMesh() {
moveInnerMeshAlongVertexNormals();
}
+ /**
+ * Appends the inner mesh to the original mesh.
+ */
private void appendInnerMesh() {
mesh.append(innerMesh);
}
+ /**
+ * Initializes data structures required for the modification process.
+ */
private void initialize() {
initializeOriginalFaces();
initializeEdgeMap();
createVertexNormals();
}
+ /**
+ * Bridges the gaps (holes) between the original and inner meshes by creating
+ * faces along unshared edges.
+ */
private void bridgeHoles() {
for (Face3D face : originalFaces) {
int size = face.indices.length;
@@ -80,6 +134,12 @@ private void bridgeHoles() {
}
}
+ /**
+ * Bridges a single hole between the original and inner meshes for the given
+ * edge.
+ *
+ * @param forwardEdge The edge of the face to bridge.
+ */
private void bridgeHole(Edge3D forwardEdge) {
Vector3f v0 = innerMesh.getVertexAt(forwardEdge.fromIndex);
Vector3f v1 = innerMesh.getVertexAt(forwardEdge.toIndex);
@@ -88,6 +148,9 @@ private void bridgeHole(Edge3D forwardEdge) {
FaceBridging.bridge(mesh, v0, v1, v2, v3);
}
+ /**
+ * Maps all edges of the mesh and stores them in a hash set.
+ */
private void mapEdges() {
for (Face3D face : mesh.faces) {
for (int i = 0; i < face.indices.length; i++) {
@@ -96,12 +159,23 @@ private void mapEdges() {
}
}
+ /**
+ * Creates an edge for the specified face at the given index.
+ *
+ * @param face The face containing the edge.
+ * @param i The index of the edge in the face.
+ * @return The edge created at the specified index.
+ */
private Edge3D createEdgeAt(Face3D face, int i) {
int fromIndex = face.indices[i];
int toIndex = face.indices[(i + 1) % face.indices.length];
return new Edge3D(fromIndex, toIndex);
}
+ /**
+ * Moves the vertices of the inner mesh along their normals by the specified
+ * thickness.
+ */
private void moveInnerMeshAlongVertexNormals() {
IntStream.range(0, innerMesh.vertices.size()).parallel().forEach(i -> {
Vector3f vertex = innerMesh.getVertexAt(i);
@@ -110,10 +184,18 @@ private void moveInnerMeshAlongVertexNormals() {
});
}
+ /**
+ * Reverses the direction of the faces in the inner mesh.
+ */
private void flipDirectionOfInnerMesh() {
innerMesh.apply(new FlipFacesModifier());
}
+ /**
+ * Validates that the provided mesh is not null.
+ *
+ * @throws IllegalArgumentException If the mesh is null.
+ */
private void validateMesh() {
if (mesh == null) {
throw new IllegalArgumentException("Mesh cannot be null.");
@@ -121,38 +203,71 @@ private void validateMesh() {
}
/**
- * Determines if the modification process can be skipped due to zero thickness
- * or an empty mesh (no vertices or faces).
+ * Determines if the modification process can be skipped due to trivial cases
+ * such as zero thickness or an empty mesh.
+ *
+ * @return True if the modification can be skipped, false otherwise.
*/
private boolean canExitEarly() {
return (thickness == 0 || mesh.vertices.isEmpty() || mesh.faces.isEmpty());
}
+ /**
+ * Initializes the inner mesh by creating a copy of the original mesh.
+ */
private void initializeInnerMesh() {
innerMesh = mesh.copy();
}
+ /**
+ * Initializes the edge map for tracking unique edges in the mesh.
+ */
private void initializeEdgeMap() {
edges = new HashSet<>();
}
+ /**
+ * Initializes the list of original faces of the mesh.
+ */
private void initializeOriginalFaces() {
originalFaces = mesh.getFaces(0, mesh.getFaceCount());
}
+ /**
+ * Computes the vertex normals for the mesh.
+ */
private void createVertexNormals() {
vertexNormals = new VertexNormals(mesh).getVertexNormals();
}
+ /**
+ * Sets the mesh to be modified.
+ *
+ * @param mesh The mesh to set.
+ */
private void setMesh(Mesh3D mesh) {
this.mesh = mesh;
}
+ /**
+ * Retrieves the thickness of the solidification process.
+ *
+ * @return The thickness value.
+ */
public float getThickness() {
return thickness;
}
+ /**
+ * Sets the thickness for the solidification process.
+ *
+ * @param thickness The thickness to set.
+ * @throws IllegalArgumentException If the thickness is negative.
+ */
public void setThickness(float thickness) {
+ if (thickness < 0) {
+ throw new IllegalArgumentException("Thickness cannot be negative.");
+ }
this.thickness = thickness;
}
diff --git a/src/main/java/workspace/examples/HelloSceneObject.java b/src/main/java/workspace/examples/HelloSceneObject.java
index 077ad10a..05bb9e8e 100644
--- a/src/main/java/workspace/examples/HelloSceneObject.java
+++ b/src/main/java/workspace/examples/HelloSceneObject.java
@@ -10,44 +10,44 @@
public class HelloSceneObject extends PApplet {
- public static void main(String[] args) {
- PApplet.main(HelloSceneObject.class.getName());
- }
-
- Mesh3D mesh;
-
- Mesh3DRenderer renderer;
-
- Workspace workspace;
-
- @Override
- public void settings() {
- size(1000, 1000, P3D);
- smooth(8);
- }
-
- @Override
- public void setup() {
- renderer = new Mesh3DRenderer(this);
- workspace = new Workspace(this);
- workspace.setGridVisible(true);
- createMesh();
- }
-
- @Override
- public void draw() {
- workspace.draw(mesh);
- }
-
- public void createMesh() {
- CubeCreator creator = new CubeCreator();
- mesh = creator.create();
-
- SceneObject sceneObject = new SceneObject();
- sceneObject.setName("Object-1");
- sceneObject.setMesh(mesh);
- sceneObject.setFillColor(new Color(255, 255, 0));
- workspace.addSceneObject(sceneObject);
- }
+ public static void main(String[] args) {
+ PApplet.main(HelloSceneObject.class.getName());
+ }
+
+ Mesh3D mesh;
+
+ Mesh3DRenderer renderer;
+
+ Workspace workspace;
+
+ @Override
+ public void settings() {
+ size(1000, 1000, P3D);
+ smooth(8);
+ }
+
+ @Override
+ public void setup() {
+ renderer = new Mesh3DRenderer(this);
+ workspace = new Workspace(this);
+ workspace.setGridVisible(true);
+ createMesh();
+ }
+
+ @Override
+ public void draw() {
+ workspace.draw(mesh);
+ }
+
+ public void createMesh() {
+ CubeCreator creator = new CubeCreator();
+ mesh = creator.create();
+
+ SceneObject sceneObject = new SceneObject();
+ sceneObject.setName("Object-1");
+ sceneObject.setMesh(mesh);
+ sceneObject.setFillColor(new Color(255, 255, 0));
+ workspace.addSceneObject(sceneObject);
+ }
}
diff --git a/src/main/java/workspace/examples/HelloWorkspace.java b/src/main/java/workspace/examples/HelloWorkspace.java
index a82741cf..4352087f 100644
--- a/src/main/java/workspace/examples/HelloWorkspace.java
+++ b/src/main/java/workspace/examples/HelloWorkspace.java
@@ -7,36 +7,36 @@
public class HelloWorkspace extends PApplet {
- public static void main(String[] args) {
- PApplet.main(HelloWorkspace.class.getName());
- }
-
- Mesh3D mesh;
-
- Workspace workspace;
-
- @Override
- public void settings() {
- size(1000, 1000, P3D);
- smooth(8);
- }
-
- @Override
- public void setup() {
- workspace = new Workspace(this);
- workspace.setGridVisible(true);
- workspace.setUiVisible(true);
- createMesh();
- }
-
- @Override
- public void draw() {
- workspace.draw(mesh);
- }
-
- public void createMesh() {
- SegmentedCubeCreator creator = new SegmentedCubeCreator();
- mesh = creator.create();
- }
+ public static void main(String[] args) {
+ PApplet.main(HelloWorkspace.class.getName());
+ }
+
+ Mesh3D mesh;
+
+ Workspace workspace;
+
+ @Override
+ public void settings() {
+ size(1000, 1000, P3D);
+ smooth(8);
+ }
+
+ @Override
+ public void setup() {
+ workspace = new Workspace(this);
+ workspace.setGridVisible(true);
+ workspace.setUiVisible(true);
+ createMesh();
+ }
+
+ @Override
+ public void draw() {
+ workspace.draw(mesh);
+ }
+
+ public void createMesh() {
+ SegmentedCubeCreator creator = new SegmentedCubeCreator();
+ mesh = creator.create();
+ }
}
diff --git a/src/test/java/bugs/BevelEdgesModifierIndexOrderRegressionTest.java b/src/test/java/bugs/BevelEdgesModifierIndexOrderRegressionTest.java
new file mode 100644
index 00000000..af58c466
--- /dev/null
+++ b/src/test/java/bugs/BevelEdgesModifierIndexOrderRegressionTest.java
@@ -0,0 +1,89 @@
+package bugs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import mesh.Face3D;
+import mesh.Mesh3D;
+import mesh.creator.platonic.IcosahedronCreator;
+import mesh.modifier.BevelEdgesModifier;
+import util.MeshTestUtil;
+
+/**
+ * Test to validate the bug fix in BevelEdgesModifier.
+ *
+ *
+ * Bug Summary: The {@code BevelEdgesModifier} produced incorrect face
+ * indices for pentagonal faces when applied to an icosphere. This issue was
+ * traced to the {@code toReverseArray} method, which incorrectly sorted indices
+ * in reverse order instead of merely reversing them.
+ *
+ *
+ *
+ * Purpose: Ensure the {@code BevelEdgesModifier} correctly generates
+ * pentagonal faces with the proper index order after the bug fix.
+ *
+ *
+ *
+ * Reproduction Details:
+ *
+ * - Create an icosphere using {@code IcosahedronCreator}.
+ * - Apply the {@code BevelEdgesModifier} with a small amount (e.g.,
+ * 0.1).
+ * - Compare the indices of generated pentagonal faces with expected
+ * values.
+ *
+ *
+ *
+ *
+ * Expected Behavior: The modifier produces pentagonal faces with index
+ * order matching the predefined {@code expected} array.
+ *
+ *
+ *
+ * Validation: This test confirms that the revised {@code toReverseArray}
+ * implementation resolves the issue, ensuring correct face generation for an
+ * icosphere.
+ *
+ */
+public class BevelEdgesModifierIndexOrderRegressionTest {
+
+ private Mesh3D mesh;
+
+ @BeforeEach
+ public void setUp() {
+ float amount = 0.1f;
+ BevelEdgesModifier modifier = new BevelEdgesModifier(amount);
+ mesh = new IcosahedronCreator().create();
+ modifier.modify(mesh);
+ }
+
+ @Test
+ public void test() {
+ int[][] expected = new int[][] { { 4, 1, 7, 10, 13 }, { 19, 2, 3, 15, 32 },
+ { 22, 8, 0, 18, 35 }, { 25, 11, 6, 21, 38 }, { 28, 14, 9, 24, 41 },
+ { 16, 5, 12, 27, 44 }, { 33, 20, 31, 45, 49 }, { 36, 23, 34, 48, 52 },
+ { 39, 26, 37, 51, 55 }, { 42, 29, 40, 54, 58 }, { 46, 30, 17, 43, 57 },
+ { 56, 53, 50, 47, 59 } };
+
+ int faceIndex = 0;
+ for (int j = 0; j < mesh.getFaceCount(); j++) {
+ Face3D face = mesh.getFaceAt(j);
+ if (face.indices.length == 5) {
+ int[] expectedIndices = expected[faceIndex];
+ int[] actualIndices = face.indices;
+ assertArrayEquals(expectedIndices, actualIndices);
+ faceIndex++;
+ }
+ }
+ }
+
+ @Test
+ public void testNormalsPointOutwards() {
+ assertTrue(MeshTestUtil.normalsPointOutwards(mesh));
+ }
+
+}