From 6cc8fb3349cc51509359f0d8c8f762c531dd1c3f Mon Sep 17 00:00:00 2001
From: Simon Dietz
- * 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:
*
+ * This modifier is commonly used in modeling workflows to add thickness to 2D
+ * surfaces or thin meshes.
+ *
+ * This modifier supports modifying all faces, a single face, or a subset of
+ * faces in a 3D mesh.
+ *
+ * Default values:
+ *
*
*
* @param a A value.
@@ -633,8 +635,8 @@ public static float log10(float a) {
*
- *
*
* @param a An angle, in radians.
@@ -398,11 +399,11 @@ public static float cos(float a) {
* Returns the trigonometric sine of an angle. Special cases:
*
*
- * The computed result must be within 1 ulp of the exact result. Results must be
- * semi-monotonic.
+ * The computed result must be within 1 ulp of the exact result. Results must
+ * be semi-monotonic.
*
* @param a An angle, in radians.
* @return The sine of the argument.
@@ -425,12 +426,12 @@ public static boolean isInRange(float a, int min, int max) {
/**
* Returns the signum function of the argument; zero if the argument is zero,
- * 1.0f if the argument is greater than zero, -1.0f if the argument is less than
- * zero. Special Cases:
+ * 1.0f if the argument is greater than zero, -1.0f if the argument is less
+ * than zero. Special Cases:
*
*
*
* @param a The floating-point value whose signum is to be returned.
@@ -441,14 +442,14 @@ public static float sign(float a) {
}
/**
- * Returns the correctly rounded positive square root of a float value. Special
- * cases:
+ * Returns the correctly rounded positive square root of a float value.
+ * Special cases:
*
*
@@ -463,26 +464,27 @@ public static float sqrt(float a) {
/**
* Returns the largest (closest to positive infinity) float value that is less
- * than or equal to the argument and is equal to a mathematical integer. Special
- * cases:
+ * than or equal to the argument and is equal to a mathematical integer.
+ * Special cases:
*
+ *
* Otherwise, the result is the double value closest to the true mathematical
* square root of the argument value.
*
*
*
* @param a A value.
- * @return The largest (closest to positive infinity) floating-point value that
- * less than or equal to the argument and is equal to a mathematical
- * integer.
+ * @return The largest (closest to positive infinity) floating-point value
+ * that less than or equal to the argument and is equal to a
+ * mathematical integer.
*/
public static float floor(float a) {
return (float) Math.floor((double) a);
}
/**
- * Returns Euler's number e raised to the power of a float value. Special cases:
+ * Returns Euler's number e raised to the power of a float value. Special
+ * cases:
*
*
- * The computed result must be within 1 ulp of the exact result. Results must be
- * semi-monotonic.
+ * The computed result must be within 1 ulp of the exact result. Results must
+ * be semi-monotonic.
*
* @param a The exponent to raise e to.
* @return The value ea, where e is the base of the natural
@@ -502,8 +504,8 @@ public static float exp(float a) {
}
/**
- * Returns true if the argument is a finite floating-point value; returns false
- * otherwise (for NaN and infinity arguments).
+ * Returns true if the argument is a finite floating-point value; returns
+ * false otherwise (for NaN and infinity arguments).
*
* @param f The float value to be tested.
* @return true if the argument is a finite floating-point value, false
@@ -514,12 +516,12 @@ public static boolean isFinite(float f) {
}
/**
- * Returns true if the specified number is infinitely large in magnitude, false
- * otherwise.
+ * Returns true if the specified number is infinitely large in magnitude,
+ * false otherwise.
*
* @param v The value to be tested.
- * @return true if the argument is positive infinity or negative infinity; false
- * otherwise.
+ * @return true if the argument is positive infinity or negative infinity;
+ * false otherwise.
*/
public static boolean isInfinite(float v) {
return Float.isInfinite(v);
@@ -569,13 +571,13 @@ public static float atan(float a) {
/**
* Returns the angle in radians whose Tan is y/x.
*
- * Return value is the angle between the x-axis and a 2D vector starting at zero
- * and terminating at (x,y).
+ * Return value is the angle between the x-axis and a 2D vector starting at
+ * zero and terminating at (x,y).
*
* @param y The ordinate coordinate.
* @param x The abscissa coordinate.
- * @return The theta component of the point (r, theta) in polar coordinates that
- * corresponds to the point (x, y) in Cartesian coordinates.
+ * @return The theta component of the point (r, theta) in polar coordinates
+ * that corresponds to the point (x, y) in Cartesian coordinates.
*/
public static float atan2(float y, float x) {
return (float) Math.atan2((double) y, (double) x);
@@ -585,8 +587,8 @@ public static float atan2(float y, float x) {
* Returns the smallest mathematical integer greater to or equal to a.
*
* @param a value
- * @return The smallest (closest to negative infinity) floating-point value that
- * is greater than or equal to the argument and is equal to a
+ * @return The smallest (closest to negative infinity) floating-point value
+ * that is greater than or equal to the argument and is equal to a
* mathematical integer.
*/
public static float ceil(float a) {
@@ -613,8 +615,8 @@ public static float pow(float a, float b) {
*
*
*
* @param a A value.
- * @return The largest (closest to positive infinity) integer value that is less
- * than or equal to the argument.
+ * @return The largest (closest to positive infinity) integer value that is
+ * less than or equal to the argument.
*/
public static int floorToInt(float a) {
return (int) Math.floor((double) a);
@@ -666,8 +668,8 @@ public static int floorToInt(float a) {
*
* @param a The value to ceil.
* @return The smallest (closest to negative infinity) integer value that is
- * greater than or equal to the argument and is equal to a mathematical
- * integer.
+ * greater than or equal to the argument and is equal to a
+ * mathematical integer.
*/
public static int ceilToInt(float a) {
return (int) Math.ceil((double) a);
@@ -675,8 +677,8 @@ public static int ceilToInt(float a) {
/**
* Linearly interpolates between a and b by t. The parameter t is not clamped
- * and values outside the range [0, 1] will result in a return value outside the
- * range [a, /b/].
+ * and values outside the range [0, 1] will result in a return value outside
+ * the range [a, /b/].
*
*
* 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");
}
From 634c495935b7bca8c40915848de7317a20032044 Mon Sep 17 00:00:00 2001
From: Simon Dietz
+ * 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
+ *
+ *
+ * The method checks that the {@code minAmount} and {@code maxAmount} values + * satisfy the following conditions: + *
+ * 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+ * * 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. *- * - * @param mesh The mesh to modify. - * @param faces The faces to be inset. - * @return The modified mesh. - * @throws IllegalArgumentException if the mesh or faces are null. */ - @Override - public Mesh3D modify(Mesh3D mesh, Collection
+ * + * 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 {@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 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. * - *
- * - * 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. - *+ * @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 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) { - 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) { - validateMesh(mesh); - return modify(mesh, mesh.getFaces()); - } - - /** - * Modifies the given mesh to create holes by insetting the specified faces to - * a specified percentage of their original size. - * - * @see #HolesModifier() - * @param mesh The mesh to modify. - * @param faces The faces to be inset. - * @return The modified mesh. - * @throws IllegalArgumentException if the mesh or faces are null. - */ - @Override - public Mesh3D modify(Mesh3D mesh, Collection
* 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); From a3ada59afb2cb9122b50b3771bc5a5d2243360c0 Mon Sep 17 00:00:00 2001 From: Simon Dietz+ * - 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 From bbcb985879403e4da241e8ce33574f43fc97c21a Mon Sep 17 00:00:00 2001 From: Simon Dietz
+ * 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: + *
+ * 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)); + } + +} From ccf18569c50252d80a4b912f967448159700e03b Mon Sep 17 00:00:00 2001 From: Simon Dietz