diff --git a/documentation/modifiers.md b/documentation/modifiers.md
index 9224707b..184c27d2 100644
--- a/documentation/modifiers.md
+++ b/documentation/modifiers.md
@@ -99,6 +99,7 @@ desired effects.
* **PushPullModifier:** Pushes or pulls vertices towards or away from a specified center point.
* **RandomHolesModifier:**
* **RemoveDoubleVerticesModifier:** Removes duplicate vertices from the mesh.
+* **RippleModifier:** Applies a ripple effect based on sinusoidal wave functions.
* **RotateXModifier:** Rotates the mesh around the X-axis.
* **RotateYModifier:** Rotates the mesh around the Y-axis.
* **RotateZModifier:** Rotates the mesh around the Z-axis.
@@ -109,6 +110,7 @@ desired effects.
* **SpherifyModifier:** Spherifies the mesh.
* **TranslateModifier:** Translates the mesh.
* **UpdateFaceNormalsModifier** Updates the face normals of the mesh.
+* **WaveModifier:** Applies a wave-like deformation, simulating the appearance of sinusoidal wave.
* **WireframeModifier:** Converts the mesh to a wireframe representation.
## Subdivision Modifiers
diff --git a/src/main/java/math/Color.java b/src/main/java/math/Color.java
index 49109681..c6b0ffab 100644
--- a/src/main/java/math/Color.java
+++ b/src/main/java/math/Color.java
@@ -325,10 +325,10 @@ public Color subtractLocal(float r, float g, float b, float a) {
* @return this
*/
public Color divideLocal(float a) {
- r /= a;
- g /= a;
- b /= a;
- a /= a;
+ this.r /= a;
+ this.g /= a;
+ this.b /= a;
+ this.a /= a;
return this;
}
diff --git a/src/main/java/math/GeometryUtil.java b/src/main/java/math/GeometryUtil.java
index ee604c5d..cdc656f6 100644
--- a/src/main/java/math/GeometryUtil.java
+++ b/src/main/java/math/GeometryUtil.java
@@ -129,7 +129,7 @@ public static double angleBetweenVectors(Vector3f v1, Vector3f v2) {
* values between 0 and 1 will return points in between.
* @return The point along the line segment.
*/
- public Vector2f getDistributionPoint(Vector2f start, Vector2f end,
+ public static Vector2f getDistributionPoint(Vector2f start, Vector2f end,
float lambda) {
float scalar = 1f / (1f + lambda);
return start.add(end.mult(lambda)).mult(scalar);
diff --git a/src/main/java/math/Mathf.java b/src/main/java/math/Mathf.java
index f70fb4c9..6f815973 100644
--- a/src/main/java/math/Mathf.java
+++ b/src/main/java/math/Mathf.java
@@ -10,948 +10,969 @@
*/
public class Mathf {
- /**
- * A random number generator used to generate random values.
- */
- private static Random random = new Random();
-
- /**
- * A float representation of the golden ratio, approximately 1.618.
- */
- public static final float GOLDEN_RATIO = (1 + sqrt(5)) / 2.0f;
-
- /**
- * A float representation of the reciprocal of the golden ratio, , which is
- * exactly 1 less than the golden ratio itself; approximately 0.618.
- */
- public static final float GOLDEN_RATIO_RECIPROCAL = 2 / (1 + sqrt(5));
-
- /**
- * Euler's number, the base of the natural logarithm, approximately 2.718.
- */
- public static final float E = (float) Math.E;
-
- /**
- * A representation of negative infinity.
- */
- public static final float NEGATIVE_INFINITY = Float.NEGATIVE_INFINITY;
-
- /**
- * A representation of positive infinity.
- */
- public static final float POSITIVE_INFINITY = Float.POSITIVE_INFINITY;
-
- /**
- * The smallest positive nonzero value representable as a float.
- */
- public static final float MIN_VALUE = Float.MIN_VALUE;
-
- /**
- * The largest finite value representable as a float.
- */
- public static final float MAX_VALUE = Float.MAX_VALUE;
-
- /**
- * A small value used for floating-point comparisons, approximately
- * 2.22E-16.
- */
- public static final double DBL_EPSILON = 2.220446049250313E-16d;
-
- /**
- * A small value used for floating-point comparisons, approximately 1.19E-7.
- */
- public static final float FLT_EPSILON = 1.1920928955078125E-7f;
-
- /**
- * A small tolerance value for comparing floating-point numbers.
- */
- public static final float ZERO_TOLERANCE = 0.0001f;
-
- /**
- * A float representation of one-third, approximately 0.33333334.
- */
- public static final float ONE_THIRD = 1f / 3f;
-
- /**
- * The value of Pi, approximately 3.14159.
- */
- public static final float PI = (float) Math.PI;
-
- /**
- * Twice the value of Pi, approximately 6.283185.
- */
- public static final float TWO_PI = 2.0f * PI;
-
- /**
- * Half the value of Pi, approximately 1.570796.
- */
- public static final float HALF_PI = 0.5f * PI;
-
- /**
- * A quarter of the value of Pi, approximately 0.785398.
- */
- public static final float QUARTER_PI = 0.25f * PI;
-
- /**
- * The reciprocal of Pi, approximately 0.3183099.
- */
- public static final float INV_PI = 1.0f / PI;
-
- /**
- * The reciprocal of two times Pi, approximately 0.1591549.
- */
- public static final float INV_TWO_PI = 1.0f / TWO_PI;
-
- /**
- * A factor to convert degrees to radians, approximately 0.0174533.
- */
- public static final float DEG_TO_RAD = PI / 180.0f;
-
- /**
- * A factor to convert radians to degrees, approximately 57.29578.
- */
- public static final float RAD_TO_DEG = 180.0f / PI;
-
- /**
- * 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.
- */
- public static final float TRIBONACCI_CONSTANT = 1.83928675521416f;
-
- /**
- * 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.
- *
- * @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.
- */
- public static int toOneDimensionalIndex(int rowIndex, int colIndex,
- int numberOfColumns) {
- if (rowIndex < 0 || colIndex < 0)
- throw new IllegalArgumentException();
-
- if (numberOfColumns <= 0)
- throw new IllegalArgumentException();
-
- return rowIndex * numberOfColumns + colIndex;
- }
-
- /**
- * Returns the smaller of two int values. That is, the result is the
- * argument closer to {@link Integer#MIN_VALUE}. If the arguments have the
- * same value, the result is that same value.
- *
- * @param a The first integer.
- * @param b The second integer.
- * @return The smaller of `a` and `b`.
- */
- public static int min(int a, int b) {
- return Math.min(a, b);
- }
-
- /**
- * Returns the larger of two int values. That is, the result is the argument
- * closer to {@link Integer#MAX_VALUE}. If the arguments have the same
- * value, the result is that same value.
- *
- * @param a The first integer.
- * @param b The second integer.
- * @return The larger of `a` and `b`.
- */
- public static int max(int a, int b) {
- return Math.max(a, b);
- }
-
- /**
- * Returns the minimum value in the given array.
- *
- * @param values The array of integers.
- * @return The minimum value in the array, or 0 if the array is empty.
- */
- public static int min(int[] values) {
- if (values.length == 0)
- return 0;
-
- int min = values[0];
- for (int i = 1; i < values.length; i++)
- min = Math.min(min, values[i]);
- return min;
- }
-
- /**
- * Returns the maximum value in the given array.
- *
- * @param values The array of integers.
- * @return The maximum value in the array, or 0 if the array is empty.
- */
- public static int max(int[] values) {
- if (values.length == 0)
- return 0;
-
- int max = values[0];
- for (int i = 1; i < values.length; i++)
- max = Math.max(max, values[i]);
- return max;
- }
-
- /**
- * Returns the larger of the two given float values.
- *
- * @param a The first float value.
- * @param b The second float value.
- * @return The larger of `a` and `b`.
- */
- public static float max(float a, float b) {
- return Math.max(a, b);
- }
-
- /**
- * Returns the smaller of the two given float values.
- *
- * @param a The first float value.
- * @param b The second float value.
- * @return The smaller of `a` and `b`.
- */
- public static float min(float a, float b) {
- return Math.min(a, 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.
- */
- public static float max(float... values) {
- if (values.length == 0)
- return Float.NaN;
-
- float max = values[0];
- for (int i = 1; i < values.length; i++)
- max = Math.max(max, values[i]);
- return max;
- }
-
- /**
- * 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.
- */
- public static float min(float... values) {
- if (values.length == 0)
- return Float.NaN;
-
- float min = values[0];
- for (int i = 1; i < values.length; i++)
- min = Math.min(min, values[i]);
- return min;
- }
-
- /**
- * Rounds a float value to the nearest integer, rounding ties towards
- * positive infinity.
- *
- * @param a The float value to be rounded.
- * @return The rounded integer value.
- */
- public static int roundToInt(float a) {
- return Math.round(a);
- }
-
- /**
- * Rounds a float value to the nearest integer.
- *
- *
- * 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.
- *
- * @param a The float value to be rounded.
- * @return The rounded float value.
- */
- public static float round(float a) {
- return Math.round(a);
- }
-
- /**
- * Clamps a value between a minimum and maximum value.
- *
- * @param a The value to clamp.
- * @param min The minimum value.
- * @param max The maximum value-
- * @return The clamped value.
- */
- public static float clamp(float a, float min, float max) {
- return Math.max(min, Math.min(max, a));
- }
-
- /**
- * Clamps a between min and max and returns the clamped value.
- *
- * @param a The value to clamp
- * @param min The minimum for a.
- * @param max The maximum for a.
- * @return The clamped value.
- */
- public static int clampInt(int a, int min, int max) {
- a = a < min ? min : (a > max ? max : a);
- return a;
- }
-
- /**
- * 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-
- * @see #saturate(float)
- */
- public static float clamp01(float a) {
- return clamp(a, 0f, 1f);
- }
-
- /**
- * Converts an angle measured in degrees to an approximately equivalent
- * angle measured in radians. The conversion from degrees to radians is
- * generally inexact; users should not expect cos(toRadians(90.0)) to
- * exactly equal 0.0.
- *
- * @param angdeg The angle, in degrees.
- * @return The angle in radians.
- */
- public static float toRadians(float angdeg) {
- return (float) Math.toRadians((double) angdeg);
- }
-
- /**
- * Converts an angle measured in radians to an approximately equivalent
- * angle measured in degrees. The conversion from radians to degreees is
- * generally inexact; users should not expect cos(toRadians(90.0)) to
- * exactlyequal 0.0.
- *
- * @param angrad The angle, in radians.
- * @return The angle in degrees.
- */
- public static float toDegrees(float angrad) {
- return (float) Math.toDegrees((double) angrad);
- }
-
- /**
- * Returns a hash code for a float value; compatible with Float.hashCode().
- *
- * @param value The value to hash.
- * @return A hash code value for a float value.
- */
- public static int hashCode(float value) {
- return Float.hashCode(value);
- }
-
- /**
- * Returns the absolute value of a.
- *
- * @param a The argument whose absolute value is to be determined.
- * @return The absolute value of the argument.
- */
- public static float abs(float a) {
- return Math.abs(a);
- }
-
- /**
- * Returns the trigonometric tangent of an angle. Special cases:
- *
- * - If the argument is NaN or an infinity, then the result is NaN.
- * - If the argument is zero, then the result is a zero with the same sign
- * as the argument.
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 tangent of the argument.
- */
- public static float tan(float a) {
- return (float) Math.tan((double) a);
- }
-
- /**
- * Returns the trigonometric cosine of an angle.
- *
- * - Special cases: If the argument is NaN or an infinity, then the result
- * is NaN.
- *
- *
- * @param a An angle, in radians.
- * @return The cosine of the argument.
- */
- public static float cos(float a) {
- return (float) Math.cos((double) a);
- }
-
- /**
- * Returns the trigonometric sine of an angle. Special cases:
- *
- * - If the argument is NaN or an infinity, then the result is NaN.
- * - If the argument is zero, then the result is a zero with the same sign
- * as the argument.
- *
- * 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.
- */
- public static float sin(float a) {
- return (float) Math.sin((double) a);
- }
-
- /**
- * Determines whether or not the given value a is in range of min and max.
- *
- * @param a The value to check.
- * @param min The minimum value for a.
- * @param max The maximum value for a.
- * @return true if the value is in range of [min,max], false otherwise.
- */
- public static boolean isInRange(float a, int min, int max) {
- return a >= min && a <= 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:
- *
- * - If the argument is NaN, then the result is NaN.
- * - If the argument is positive zero or negative zero, then the result is
- * the same as the argument.
- *
- *
- * @param a The floating-point value whose signum is to be returned.
- * @return The signum function of the argument.
- */
- public static float sign(float a) {
- return Math.signum(a);
- }
-
- /**
- * Returns the correctly rounded positive square root of a float value.
- * Special cases:
- *
- * - If the argument is NaN or less than zero, then the result is
- * NaN.
- * - If the argument is positive infinity, then the result is positive
- * infinity.
- * - If the argument is positive zero or negative zero, then the result is
- * the same as the argument.
- * Otherwise, the result is the double value closest to the true
- * mathematical square root of the argument value.
- *
- *
- * @param a A value.
- * @return The positive square root of a. If the argument is NaN or less
- * than zero, the result is NaN.
- */
- public static float sqrt(float a) {
- return (float) Math.sqrt((double) 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:
- *
- * - If the argument value is already equal to a mathematical integer,
- * then the result is the same as the argument.
- * - If the argument is NaN or an infinity or positive zero or negative
- * zero, then the result is the same as the argument.
- *
- *
- * @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.
- */
- 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:
- *
- * - If the argument is NaN, the result is NaN.
- * - If the argument is positive infinity, then the result is positive
- * infinity.
- * - If the argument is negative infinity, then the result is positive
- * zero.
- *
- * 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
- * logarithms.
- */
- public static float exp(float a) {
- return (float) Math.exp((double) a);
- }
-
- /**
- * 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
- * otherwise.
- */
- public static boolean isFinite(float f) {
- return Float.isFinite(f);
- }
-
- /**
- * 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.
- */
- public static boolean isInfinite(float v) {
- return Float.isInfinite(v);
- }
-
- /**
- * Returns true if the specified number is a Not-a-Number (NaN) value, false
- * otherwise.
- *
- * @param v The value to be tested.
- * @return true if the argument is NaN; false otherwise.
- */
- public static boolean isNaN(float v) {
- return Float.isNaN(v);
- }
-
- /**
- * Returns the arc cosine of a (the angle in radians whose cosine is a).
- *
- * @param a The value whose arc cosine is to be returned.
- * @return The arc cosine of a.
- */
- public static float acos(float a) {
- return (float) Math.acos((double) a);
- }
-
- /**
- * Returns the arc-sine of a - the angle in radians whose sine is a.
- *
- * @param a The value whose arc sine is to be returned.
- * @return The arc sine of the argument.
- */
- public static float asin(float a) {
- return (float) Math.asin((double) a);
- }
-
- /**
- * Returns the arc tangent of a (the angle in radians whose tangent is a).
- *
- * @param a The value whose arc tangent is to be returned.
- * @return The arc tangent of the argument.
- */
- public static float atan(float a) {
- return (float) Math.atan((double) 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).
- *
- * @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.
- */
- public static float atan2(float y, float x) {
- return (float) Math.atan2((double) y, (double) 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
- * mathematical integer.
- */
- public static float ceil(float a) {
- return (float) Math.ceil((double) a);
- }
-
- /**
- * Returns the value of the first argument raised to the power of the second
- * argument.
- *
- * @param a The base.
- * @param b The exponent.
- * @return The base raised to the power of b.
- * @see Math#pow(double, double)
- */
- public static float pow(float a, float b) {
- return (float) Math.pow((double) a, (double) b);
- }
-
- /**
- * Returns the base 10 logarithm of a double value. Special cases:
- *
- * - If the argument is NaN or less than zero, then the result is
- * NaN.
- * - If the argument is positive infinity, then the result is positive
- * infinity.
- * - If the argument is positive zero or negative zero, then the result is
- * negative infinity.
- *
- *
- * @param a A value.
- * @return The base 10 logarithm of a.
- */
- public static float log10(float a) {
- return (float) Math.log10((double) a);
- }
-
- /**
- * Returns the natural logarithm (base e) of a float value. Special cases:
- *
- * - If the argument is NaN or less than zero, then the result is
- * NaN.
- * - If the argument is positive infinity, then the result is positive
- * infinity.
- * - If the argument is positive zero or negative zero, then the result is
- * negative infinity.
- *
- * The computed result must be within 1 ulp of the exact result. Results
- * must be semi-monotonic.
- *
- * @param a A value.
- * @return The value ln a, the natural logarithm of a.
- */
- public static float log(float a) {
- return (float) Math.log((double) a);
- }
-
- /**
- * Returns the largest (closest to positive infinity) integer value that is
- * less than or equal to the argument.
- *
- * - If the argument value is already equal to a mathematical integer,
- * then the result is the same as the argument.
- * - If the argument is NaN or an infinity or positive zero or negative
- * zero, then the result is the same as the argument.
- *
- *
- * @param a A value.
- * @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);
- }
-
- /**
- * Returns the smallest mathematical integer greater to or equal to 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.
- */
- public static int ceilToInt(float a) {
- return (int) Math.ceil((double) 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/].
- *
- *
- * When t = 0 returns a.
- * When t = 1 returns b.
- * When t = 0.5 returns the midpoint of a and b.
- *
- *
- * @param a The value to interpolate from.
- * @param b The value to interpolate to.
- * @param t The value to interpolate by.
- * @return The resultant interpolated value.
- */
- public static float lerpUnclamped(float a, float b, float t) {
- return a + (b - a) * t;
- }
-
- /**
- * Linearly interpolates between from and to by t. The parameter t is
- * clamped to the range [0, 1].
- *
- *
- * When t = 0 returns a.
- * When t = 1 return b.
- * When t = 0.5 returns the midpoint of a and b.
- *
- *
- * @param from The value to interpolate from.
- * @param to The value to interpolate to.
- * @param t The value to interpolate by.
- * @return The resultant interpolated value.
- */
- public static float lerp(float from, float to, float t) {
- return from + (to - from) * clamp01(t);
- }
-
- /**
- * Returns the next power of two greater than or equal to the given value.
- *
- *
- * For example:
- *
- * - nextPowerOfTwo(1) returns 2
- * - nextPowerOfTwo(2) returns 2
- * - nextPowerOfTwo(3) returns 4
- * - nextPowerOfTwo(16) returns 16
- * - nextPowerOfTwo(17) returns 32
- *
- *
- * @param value the input value
- * @return the next power of two greater than or equal to the input value
- */
- public static int nextPowerOfTwo(int value) {
- return value > 0 ? Integer.highestOneBit(value - 1) << 1 : 1;
- }
-
- /**
- * 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.
- *
- * @param from The starting value.
- * @param to The ending value.
- * @param t The interpolation factor, clamped to the range [0, 1].
- * @return The interpolated value.
- */
- public static float smoothStep(float from, float to, float t) {
- t = clamp01(t);
- t = -2f * t * t * t + 3f * t * t;
- return to * t + from * (1f - t);
- }
-
- /**
- * Returns a random float value between the specified minimum and maximum
- * values, inclusive.
- *
- * @param min The minimum value.
- * @param max The maximum value.
- * @return A random float value between min and max, inclusive.
- */
- public static float random(float min, float max) {
- return random.nextFloat() * (max - min) + min;
- }
-
- /**
- * Returns a random integer value between the specified minimum and maximum
- * values, inclusive.
- *
- * @param min The minimum value.
- * @param max The maximum value.
- * @return A random integer value between min and max, inclusive.
- */
- public static int random(int min, int max) {
- return random.nextInt(max - min + 1) + min;
- }
-
- /**
- * Sets the seed for the random number generator. This allows for
- * reproducible random sequences.
- *
- * @param seed The seed value.
- */
- public static void setSeed(long seed) {
- random.setSeed(seed);
- }
-
- /**
- * Returns a random float value between 0.0 (inclusive) and 1.0 (exclusive).
- *
- * This method uses a random number generator with a specified seed to
- * ensure reproducibility.
- *
- * @return A random float value between 0.0 (inclusive) and 1.0 (exclusive).
- */
- public static float randomFloat() {
- return random.nextFloat();
- }
-
- /**
- * 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.
- *
- * 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.
- *
- * @param t The input time.
- * @param length The desired range of oscillation.
- * @return The calculated oscillating value.
- */
- public static float pingPong(float t, float length) {
- t = repeat(t, length * 2f);
- return length - abs(t - length);
- }
-
- /**
- * 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π.
- *
- * @param a The angle to be normalized.
- * @param center The center angle of the desired range.
- * @return The normalized angle.
- */
- public static float normalizeAngle(float a, float center) {
- return a - TWO_PI * floor((a + PI - center) / TWO_PI);
- }
-
- /**
- * 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.
- *
- * **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.
- * @return The wrapped value within the interval [0, length).
- */
- public static float repeat(float t, float length) {
- return t - floor(t / length) * 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.
- *
- * **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.
- *
- * **Why such a method is necessary:** Due to the limited precision of
- * floating-point numbers, small rounding errors can occur, causing two
- * mathematically equal values to not be represented exactly equal in a
- * computer. This method allows ignoring such small differences.
- *
- * @param a The first floating-point number.
- * @param b The second floating-point number.
- * @return `true` if `a` and `b` are approximately equal, otherwise `false`.
- */
- public static boolean approximately(float a, float b) {
- return abs(b - a) < max(1E-06f * max(abs(a), abs(b)), FLT_EPSILON * 8f);
- }
-
- /**
- * 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.
- * @see #clamp01(float)
- */
- public static float saturate(float a) {
- return clamp(a, 0f, 1f);
- }
-
- /**
- * Returns the next power of two of the given value.
- *
- * E.g. for a value of 100, this returns 128. Returns 1 for all numbers <=
- * 1.
- *
- * @param value The number to obtain the power of two for.
- * @return The closest power of two.
- */
- public static int closestPowerOfTwo(int value) {
- value--;
- value |= value >> 1;
- value |= value >> 2;
- value |= value >> 4;
- value |= value >> 8;
- value |= value >> 16;
- value++;
- value += (value == 0) ? 1 : 0;
- return value;
- }
-
- /**
- * Maps a value from one range to another using linear interpolation.
- *
- * @param value The value to be mapped.
- * @param from0 The lower bound of the input range.
- * @param to0 The upper bound of the input range.
- * @param from1 The lower bound of the output range.
- * @param to1 The upper bound of the output range.
- * @return The mapped value.
- *
- * @throws IllegalArgumentException if `from0 == to0` or `from1 == 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");
- }
-
- float result = from1
- + (to1 - from1) * ((value - from0) / (to0 - from0));
-
- if (Float.isNaN(result)) {
- throw new IllegalArgumentException("Result is NaN");
- } else if (Float.isInfinite(result)) {
- throw new IllegalArgumentException("Result is infinite");
- }
-
- return result;
- }
+ /**
+ * A random number generator used to generate random values.
+ */
+ private static Random random = new Random();
+
+ /**
+ * A float representation of the golden ratio, approximately 1.618.
+ */
+ public static final float GOLDEN_RATIO = (1 + sqrt(5)) / 2.0f;
+
+ /**
+ * A float representation of the reciprocal of the golden ratio, , which is
+ * exactly 1 less than the golden ratio itself; approximately 0.618.
+ */
+ public static final float GOLDEN_RATIO_RECIPROCAL = 2 / (1 + sqrt(5));
+
+ /**
+ * Euler's number, the base of the natural logarithm, approximately 2.718.
+ */
+ public static final float E = (float) Math.E;
+
+ /**
+ * A representation of negative infinity.
+ */
+ public static final float NEGATIVE_INFINITY = Float.NEGATIVE_INFINITY;
+
+ /**
+ * A representation of positive infinity.
+ */
+ public static final float POSITIVE_INFINITY = Float.POSITIVE_INFINITY;
+
+ /**
+ * The smallest positive nonzero value representable as a float.
+ */
+ public static final float MIN_VALUE = Float.MIN_VALUE;
+
+ /**
+ * The largest finite value representable as a float.
+ */
+ public static final float MAX_VALUE = Float.MAX_VALUE;
+
+ /**
+ * A small value used for floating-point comparisons, approximately 2.22E-16.
+ */
+ public static final double DBL_EPSILON = 2.220446049250313E-16d;
+
+ /**
+ * A small value used for floating-point comparisons, approximately 1.19E-7.
+ */
+ public static final float FLT_EPSILON = 1.1920928955078125E-7f;
+
+ /**
+ * A small tolerance value for comparing floating-point numbers.
+ */
+ public static final float ZERO_TOLERANCE = 0.0001f;
+
+ /**
+ * A float representation of one-third, approximately 0.33333334.
+ */
+ public static final float ONE_THIRD = 1f / 3f;
+
+ /**
+ * The value of Pi, approximately 3.14159.
+ */
+ public static final float PI = (float) Math.PI;
+
+ /**
+ * Twice the value of Pi, approximately 6.283185.
+ */
+ public static final float TWO_PI = 2.0f * PI;
+
+ /**
+ * Half the value of Pi, approximately 1.570796.
+ */
+ public static final float HALF_PI = 0.5f * PI;
+
+ /**
+ * A quarter of the value of Pi, approximately 0.785398.
+ */
+ public static final float QUARTER_PI = 0.25f * PI;
+
+ /**
+ * The reciprocal of Pi, approximately 0.3183099.
+ */
+ public static final float INV_PI = 1.0f / PI;
+
+ /**
+ * The reciprocal of two times Pi, approximately 0.1591549.
+ */
+ public static final float INV_TWO_PI = 1.0f / TWO_PI;
+
+ /**
+ * A factor to convert degrees to radians, approximately 0.0174533.
+ */
+ public static final float DEG_TO_RAD = PI / 180.0f;
+
+ /**
+ * A factor to convert radians to degrees, approximately 57.29578.
+ */
+ public static final float RAD_TO_DEG = 180.0f / PI;
+
+ /**
+ * 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.
+ */
+ public static final float TRIBONACCI_CONSTANT = 1.83928675521416f;
+
+ /**
+ * 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.
+ *
+ * @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.
+ */
+ public static int toOneDimensionalIndex(int rowIndex, int colIndex, int numberOfColumns) {
+ if (rowIndex < 0 || colIndex < 0)
+ throw new IllegalArgumentException();
+
+ if (numberOfColumns <= 0)
+ throw new IllegalArgumentException();
+
+ return rowIndex * numberOfColumns + colIndex;
+ }
+
+ /**
+ * Returns the smaller of two int values. That is, the result is the argument
+ * closer to {@link Integer#MIN_VALUE}. If the arguments have the same value,
+ * the result is that same value.
+ *
+ * @param a The first integer.
+ * @param b The second integer.
+ * @return The smaller of `a` and `b`.
+ */
+ public static int min(int a, int b) {
+ return Math.min(a, b);
+ }
+
+ /**
+ * Returns the larger of two int values. That is, the result is the argument
+ * closer to {@link Integer#MAX_VALUE}. If the arguments have the same value,
+ * the result is that same value.
+ *
+ * @param a The first integer.
+ * @param b The second integer.
+ * @return The larger of `a` and `b`.
+ */
+ public static int max(int a, int b) {
+ return Math.max(a, b);
+ }
+
+ /**
+ * Returns the minimum value in the given array.
+ *
+ * @param values The array of integers.
+ * @return The minimum value in the array, or 0 if the array is empty.
+ */
+ public static int min(int[] values) {
+ if (values.length == 0)
+ return 0;
+
+ int min = values[0];
+ for (int i = 1; i < values.length; i++)
+ min = Math.min(min, values[i]);
+ return min;
+ }
+
+ /**
+ * Returns the maximum value in the given array.
+ *
+ * @param values The array of integers.
+ * @return The maximum value in the array, or 0 if the array is empty.
+ */
+ public static int max(int[] values) {
+ if (values.length == 0)
+ return 0;
+
+ int max = values[0];
+ for (int i = 1; i < values.length; i++)
+ max = Math.max(max, values[i]);
+ return max;
+ }
+
+ /**
+ * Returns the larger of the two given float values.
+ *
+ * @param a The first float value.
+ * @param b The second float value.
+ * @return The larger of `a` and `b`.
+ */
+ public static float max(float a, float b) {
+ return Math.max(a, b);
+ }
+
+ /**
+ * Returns the smaller of the two given float values.
+ *
+ * @param a The first float value.
+ * @param b The second float value.
+ * @return The smaller of `a` and `b`.
+ */
+ public static float min(float a, float b) {
+ return Math.min(a, 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.
+ */
+ public static float max(float... values) {
+ if (values.length == 0)
+ return Float.NaN;
+
+ float max = values[0];
+ for (int i = 1; i < values.length; i++)
+ max = Math.max(max, values[i]);
+ return max;
+ }
+
+ /**
+ * 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.
+ */
+ public static float min(float... values) {
+ if (values.length == 0)
+ return Float.NaN;
+
+ float min = values[0];
+ for (int i = 1; i < values.length; i++)
+ min = Math.min(min, values[i]);
+ return min;
+ }
+
+ /**
+ * Rounds a float value to the nearest integer, rounding ties towards positive
+ * infinity.
+ *
+ * @param a The float value to be rounded.
+ * @return The rounded integer value.
+ */
+ public static int roundToInt(float a) {
+ return Math.round(a);
+ }
+
+ /**
+ * Rounds a float value to the nearest integer.
+ *
+ *
+ * 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.
+ *
+ * @param a The float value to be rounded.
+ * @return The rounded float value.
+ */
+ public static float round(float a) {
+ return Math.round(a);
+ }
+
+ /**
+ * Clamps a value between a minimum and maximum value.
+ *
+ * @param a The value to clamp.
+ * @param min The minimum value.
+ * @param max The maximum value-
+ * @return The clamped value.
+ */
+ public static float clamp(float a, float min, float max) {
+ return Math.max(min, Math.min(max, a));
+ }
+
+ /**
+ * Clamps a between min and max and returns the clamped value.
+ *
+ * @param a The value to clamp
+ * @param min The minimum for a.
+ * @param max The maximum for a.
+ * @return The clamped value.
+ */
+ public static int clampInt(int a, int min, int max) {
+ a = a < min ? min : (a > max ? max : a);
+ return a;
+ }
+
+ /**
+ * 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-
+ * @see #saturate(float)
+ */
+ public static float clamp01(float a) {
+ return clamp(a, 0f, 1f);
+ }
+
+ /**
+ * Converts an angle measured in degrees to an approximately equivalent angle
+ * measured in radians. The conversion from degrees to radians is generally
+ * inexact; users should not expect cos(toRadians(90.0)) to exactly equal 0.0.
+ *
+ * @param angdeg The angle, in degrees.
+ * @return The angle in radians.
+ */
+ public static float toRadians(float angdeg) {
+ return (float) Math.toRadians((double) angdeg);
+ }
+
+ /**
+ * Converts an angle measured in radians to an approximately equivalent angle
+ * measured in degrees. The conversion from radians to degreees is generally
+ * inexact; users should not expect cos(toRadians(90.0)) to exactlyequal 0.0.
+ *
+ * @param angrad The angle, in radians.
+ * @return The angle in degrees.
+ */
+ public static float toDegrees(float angrad) {
+ return (float) Math.toDegrees((double) angrad);
+ }
+
+ /**
+ * Returns a hash code for a float value; compatible with Float.hashCode().
+ *
+ * @param value The value to hash.
+ * @return A hash code value for a float value.
+ */
+ public static int hashCode(float value) {
+ return Float.hashCode(value);
+ }
+
+ /**
+ * Returns the absolute value of a.
+ *
+ * @param a The argument whose absolute value is to be determined.
+ * @return The absolute value of the argument.
+ */
+ public static float abs(float a) {
+ return Math.abs(a);
+ }
+
+ /**
+ * Returns the trigonometric tangent of an angle. Special cases:
+ *
+ * - If the argument is NaN or an infinity, then the result is NaN.
+ * - If the argument is zero, then the result is a zero with the same sign as
+ * the argument.
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 tangent of the argument.
+ */
+ public static float tan(float a) {
+ return (float) Math.tan((double) a);
+ }
+
+ /**
+ * Returns the trigonometric cosine of an angle.
+ *
+ * - Special cases: If the argument is NaN or an infinity, then the result is
+ * NaN.
+ *
+ *
+ * @param a An angle, in radians.
+ * @return The cosine of the argument.
+ */
+ public static float cos(float a) {
+ return (float) Math.cos((double) a);
+ }
+
+ /**
+ * Returns the trigonometric sine of an angle. Special cases:
+ *
+ * - If the argument is NaN or an infinity, then the result is NaN.
+ * - If the argument is zero, then the result is a zero with the same sign as
+ * the argument.
+ *
+ * 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.
+ */
+ public static float sin(float a) {
+ return (float) Math.sin((double) a);
+ }
+
+ /**
+ * Determines whether or not the given value a is in range of min and max.
+ *
+ * @param a The value to check.
+ * @param min The minimum value for a.
+ * @param max The maximum value for a.
+ * @return true if the value is in range of [min,max], false otherwise.
+ */
+ public static boolean isInRange(float a, int min, int max) {
+ return a >= min && a <= 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:
+ *
+ * - If the argument is NaN, then the result is NaN.
+ * - If the argument is positive zero or negative zero, then the result is the
+ * same as the argument.
+ *
+ *
+ * @param a The floating-point value whose signum is to be returned.
+ * @return The signum function of the argument.
+ */
+ public static float sign(float a) {
+ return Math.signum(a);
+ }
+
+ /**
+ * Returns the correctly rounded positive square root of a float value. Special
+ * cases:
+ *
+ * - If the argument is NaN or less than zero, then the result is NaN.
+ * - If the argument is positive infinity, then the result is positive
+ * infinity.
+ * - If the argument is positive zero or negative zero, then the result is the
+ * same as the argument.
+ * Otherwise, the result is the double value closest to the true mathematical
+ * square root of the argument value.
+ *
+ *
+ * @param a A value.
+ * @return The positive square root of a. If the argument is NaN or less than
+ * zero, the result is NaN.
+ */
+ public static float sqrt(float a) {
+ return (float) Math.sqrt((double) 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:
+ *
+ * - If the argument value is already equal to a mathematical integer, then
+ * the result is the same as the argument.
+ * - If the argument is NaN or an infinity or positive zero or negative zero,
+ * then the result is the same as the argument.
+ *
+ *
+ * @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.
+ */
+ 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:
+ *
+ * - If the argument is NaN, the result is NaN.
+ * - If the argument is positive infinity, then the result is positive
+ * infinity.
+ * - If the argument is negative infinity, then the result is positive
+ * zero.
+ *
+ * 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
+ * logarithms.
+ */
+ public static float exp(float a) {
+ return (float) Math.exp((double) a);
+ }
+
+ /**
+ * 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
+ * otherwise.
+ */
+ public static boolean isFinite(float f) {
+ return Float.isFinite(f);
+ }
+
+ /**
+ * 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.
+ */
+ public static boolean isInfinite(float v) {
+ return Float.isInfinite(v);
+ }
+
+ /**
+ * Returns true if the specified number is a Not-a-Number (NaN) value, false
+ * otherwise.
+ *
+ * @param v The value to be tested.
+ * @return true if the argument is NaN; false otherwise.
+ */
+ public static boolean isNaN(float v) {
+ return Float.isNaN(v);
+ }
+
+ /**
+ * Returns the arc cosine of a (the angle in radians whose cosine is a).
+ *
+ * @param a The value whose arc cosine is to be returned.
+ * @return The arc cosine of a.
+ */
+ public static float acos(float a) {
+ return (float) Math.acos((double) a);
+ }
+
+ /**
+ * Returns the arc-sine of a - the angle in radians whose sine is a.
+ *
+ * @param a The value whose arc sine is to be returned.
+ * @return The arc sine of the argument.
+ */
+ public static float asin(float a) {
+ return (float) Math.asin((double) a);
+ }
+
+ /**
+ * Returns the arc tangent of a (the angle in radians whose tangent is a).
+ *
+ * @param a The value whose arc tangent is to be returned.
+ * @return The arc tangent of the argument.
+ */
+ public static float atan(float a) {
+ return (float) Math.atan((double) 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).
+ *
+ * @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.
+ */
+ public static float atan2(float y, float x) {
+ return (float) Math.atan2((double) y, (double) 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
+ * mathematical integer.
+ */
+ public static float ceil(float a) {
+ return (float) Math.ceil((double) a);
+ }
+
+ /**
+ * Returns the value of the first argument raised to the power of the second
+ * argument.
+ *
+ * @param a The base.
+ * @param b The exponent.
+ * @return The base raised to the power of b.
+ * @see Math#pow(double, double)
+ */
+ public static float pow(float a, float b) {
+ return (float) Math.pow((double) a, (double) b);
+ }
+
+ /**
+ * Returns the base 10 logarithm of a double value. Special cases:
+ *
+ * - If the argument is NaN or less than zero, then the result is NaN.
+ * - If the argument is positive infinity, then the result is positive
+ * infinity.
+ * - If the argument is positive zero or negative zero, then the result is
+ * negative infinity.
+ *
+ *
+ * @param a A value.
+ * @return The base 10 logarithm of a.
+ */
+ public static float log10(float a) {
+ return (float) Math.log10((double) a);
+ }
+
+ /**
+ * Returns the natural logarithm (base e) of a float value. Special cases:
+ *
+ * - If the argument is NaN or less than zero, then the result is NaN.
+ * - If the argument is positive infinity, then the result is positive
+ * infinity.
+ * - If the argument is positive zero or negative zero, then the result is
+ * negative infinity.
+ *
+ * The computed result must be within 1 ulp of the exact result. Results must be
+ * semi-monotonic.
+ *
+ * @param a A value.
+ * @return The value ln a, the natural logarithm of a.
+ */
+ public static float log(float a) {
+ return (float) Math.log((double) a);
+ }
+
+ /**
+ * Returns the largest (closest to positive infinity) integer value that is less
+ * than or equal to the argument.
+ *
+ * - If the argument value is already equal to a mathematical integer, then
+ * the result is the same as the argument.
+ * - If the argument is NaN or an infinity or positive zero or negative zero,
+ * then the result is the same as the argument.
+ *
+ *
+ * @param a A value.
+ * @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);
+ }
+
+ /**
+ * Returns the smallest mathematical integer greater to or equal to 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.
+ */
+ public static int ceilToInt(float a) {
+ return (int) Math.ceil((double) 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/].
+ *
+ *
+ * When t = 0 returns a.
+ * When t = 1 returns b.
+ * When t = 0.5 returns the midpoint of a and b.
+ *
+ *
+ * @param a The value to interpolate from.
+ * @param b The value to interpolate to.
+ * @param t The value to interpolate by.
+ * @return The resultant interpolated value.
+ */
+ public static float lerpUnclamped(float a, float b, float t) {
+ return a + (b - a) * t;
+ }
+
+ /**
+ * Linearly interpolates between from and to by t. The parameter t is clamped to
+ * the range [0, 1].
+ *
+ *
+ * When t = 0 returns a.
+ * When t = 1 return b.
+ * When t = 0.5 returns the midpoint of a and b.
+ *
+ *
+ * @param from The value to interpolate from.
+ * @param to The value to interpolate to.
+ * @param t The value to interpolate by.
+ * @return The resultant interpolated value.
+ */
+ public static float lerp(float from, float to, float t) {
+ return from + (to - from) * clamp01(t);
+ }
+
+ /**
+ * Returns the next power of two greater than or equal to the given value.
+ *
+ *
+ * For example:
+ *
+ * - nextPowerOfTwo(1) returns 2
+ * - nextPowerOfTwo(2) returns 2
+ * - nextPowerOfTwo(3) returns 4
+ * - nextPowerOfTwo(16) returns 16
+ * - nextPowerOfTwo(17) returns 32
+ *
+ *
+ * @param value the input value
+ * @return the next power of two greater than or equal to the input value
+ */
+ public static int nextPowerOfTwo(int value) {
+ return value > 0 ? Integer.highestOneBit(value - 1) << 1 : 1;
+ }
+
+ /**
+ * 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.
+ *
+ * @param from The starting value.
+ * @param to The ending value.
+ * @param t The interpolation factor, clamped to the range [0, 1].
+ * @return The interpolated value.
+ */
+ public static float smoothStep(float from, float to, float t) {
+ t = clamp01(t);
+ t = -2f * t * t * t + 3f * t * t;
+ return to * t + from * (1f - t);
+ }
+
+ /**
+ * Returns a random float value between the specified minimum and maximum
+ * values, inclusive.
+ *
+ * @param min The minimum value.
+ * @param max The maximum value.
+ * @return A random float value between min and max, inclusive.
+ */
+ public static float random(float min, float max) {
+ return random.nextFloat() * (max - min) + min;
+ }
+
+ /**
+ * Returns a random integer value between the specified minimum and maximum
+ * values, inclusive.
+ *
+ * @param min The minimum value.
+ * @param max The maximum value.
+ * @return A random integer value between min and max, inclusive.
+ */
+ public static int random(int min, int max) {
+ return random.nextInt(max - min + 1) + min;
+ }
+
+ /**
+ * Sets the seed for the random number generator. This allows for reproducible
+ * random sequences.
+ *
+ * @param seed The seed value.
+ */
+ public static void setSeed(long seed) {
+ random.setSeed(seed);
+ }
+
+ /**
+ * Returns a random float value between 0.0 (inclusive) and 1.0 (exclusive).
+ *
+ * This method uses a random number generator with a specified seed to ensure
+ * reproducibility.
+ *
+ * @return A random float value between 0.0 (inclusive) and 1.0 (exclusive).
+ */
+ public static float randomFloat() {
+ return random.nextFloat();
+ }
+
+ /**
+ * 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.
+ *
+ * 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.
+ *
+ * @param t The input time.
+ * @param length The desired range of oscillation.
+ * @return The calculated oscillating value.
+ */
+ public static float pingPong(float t, float length) {
+ t = repeat(t, length * 2f);
+ return length - abs(t - length);
+ }
+
+ /**
+ * 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π.
+ *
+ * @param a The angle to be normalized.
+ * @param center The center angle of the desired range.
+ * @return The normalized angle.
+ */
+ public static float normalizeAngle(float a, float center) {
+ return a - TWO_PI * floor((a + PI - center) / TWO_PI);
+ }
+
+ /**
+ * 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.
+ *
+ * **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.
+ * @return The wrapped value within the interval [0, length).
+ */
+ public static float repeat(float t, float length) {
+ return t - floor(t / length) * 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.
+ *
+ * **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.
+ *
+ * **Why such a method is necessary:** Due to the limited precision of
+ * floating-point numbers, small rounding errors can occur, causing two
+ * mathematically equal values to not be represented exactly equal in a
+ * computer. This method allows ignoring such small differences.
+ *
+ * @param a The first floating-point number.
+ * @param b The second floating-point number.
+ * @return `true` if `a` and `b` are approximately equal, otherwise `false`.
+ */
+ public static boolean approximately(float a, float b) {
+ return abs(b - a) < max(1E-06f * max(abs(a), abs(b)), FLT_EPSILON * 8f);
+ }
+
+ /**
+ * 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.
+ * @see #clamp01(float)
+ */
+ public static float saturate(float a) {
+ return clamp(a, 0f, 1f);
+ }
+
+ /**
+ * Returns the next power of two of the given value.
+ *
+ * E.g. for a value of 100, this returns 128. Returns 1 for all numbers <= 1.
+ *
+ * @param value The number to obtain the power of two for.
+ * @return The closest power of two.
+ */
+ public static int closestPowerOfTwo(int value) {
+ value--;
+ value |= value >> 1;
+ value |= value >> 2;
+ value |= value >> 4;
+ value |= value >> 8;
+ value |= value >> 16;
+ value++;
+ value += (value == 0) ? 1 : 0;
+ return value;
+ }
+
+ /**
+ * Maps a value from one range to another using linear interpolation.
+ *
+ * @param value The value to be mapped.
+ * @param from0 The lower bound of the input range.
+ * @param to0 The upper bound of the input range.
+ * @param from1 The lower bound of the output range.
+ * @param to1 The upper bound of the output range.
+ * @return The mapped value.
+ *
+ * @throws IllegalArgumentException if `from0 == to0` or `from1 == 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");
+ }
+
+ float result = from1 + (to1 - from1) * ((value - from0) / (to0 - from0));
+
+ if (Float.isNaN(result)) {
+ throw new IllegalArgumentException("Result is NaN");
+ } else if (Float.isInfinite(result)) {
+ throw new IllegalArgumentException("Result is infinite");
+ }
+
+ return result;
+ }
+
+ /**
+ * Calculates the floating-point remainder of dividing two values. This method
+ * works similarly to the fmod function in other programming languages.
+ *
+ * @param a the dividend
+ * @param b the divisor
+ * @return the remainder when a is divided by b
+ */
+ public static float fmod(float a, float b) {
+ return (float) (a - b * Math.floor(a / b));
+ }
+
+ /**
+ * Normalizes the input angle to the range [0, 2π] in radians.
+ *
+ * Small values close to zero (less than 1e-6) are snapped to zero to handle
+ * floating-point precision issues.
+ *
+ * @param angle The input angle in radians.
+ * @return The normalized angle in the range [0, 2π].
+ */
+ public static float normalizeAngle(float angle) {
+ float smallAngleThreshold = 1e-6f;
+
+ angle = angle % (2 * Mathf.PI);
+
+ if (Mathf.abs(angle) < smallAngleThreshold) {
+ angle = 0;
+ }
+
+ if (angle < 0) {
+ angle += 2 * Mathf.PI;
+ }
+
+ return angle;
+ }
}
diff --git a/src/main/java/math/Vector3f.java b/src/main/java/math/Vector3f.java
index d56d1623..7d756a56 100644
--- a/src/main/java/math/Vector3f.java
+++ b/src/main/java/math/Vector3f.java
@@ -424,14 +424,34 @@ public Vector3f maxLocal(Vector3f a, Vector3f b) {
}
public Vector3f lerpLocal(Vector3f finalVec, float changeAmnt) {
+ if (changeAmnt == 0) {
+ return this;
+ }
+ if (changeAmnt == 1) {
+ this.x = finalVec.x;
+ this.y = finalVec.y;
+ this.z = finalVec.z;
+ return this;
+ }
this.x = (1 - changeAmnt) * this.x + changeAmnt * finalVec.x;
this.y = (1 - changeAmnt) * this.y + changeAmnt * finalVec.y;
this.z = (1 - changeAmnt) * this.z + changeAmnt * finalVec.z;
return this;
}
- public Vector3f lerpLocal(Vector3f beginVec, Vector3f finalVec,
- float changeAmnt) {
+ public Vector3f lerpLocal(Vector3f beginVec, Vector3f finalVec, float changeAmnt) {
+ if (changeAmnt == 0) {
+ this.x = beginVec.x;
+ this.y = beginVec.y;
+ this.z = beginVec.z;
+ return this;
+ }
+ if (changeAmnt == 1) {
+ this.x = finalVec.x;
+ this.y = finalVec.y;
+ this.z = finalVec.z;
+ return this;
+ }
this.x = (1 - changeAmnt) * beginVec.x + changeAmnt * finalVec.x;
this.y = (1 - changeAmnt) * beginVec.y + changeAmnt * finalVec.y;
this.z = (1 - changeAmnt) * beginVec.z + changeAmnt * finalVec.z;
diff --git a/src/main/java/mesh/Mesh3D.java b/src/main/java/mesh/Mesh3D.java
index e2d9ff34..045d11dc 100644
--- a/src/main/java/mesh/Mesh3D.java
+++ b/src/main/java/mesh/Mesh3D.java
@@ -5,108 +5,141 @@
import java.util.Collection;
import java.util.List;
-import math.Mathf;
-import math.Matrix3f;
import math.Vector3f;
import mesh.modifier.IMeshModifier;
import mesh.modifier.RemoveDoubleVerticesModifier;
+import mesh.modifier.RotateYModifier;
+import mesh.modifier.RotateZModifier;
+import mesh.modifier.TranslateModifier;
import mesh.util.Bounds3;
public class Mesh3D {
- public Vector3f translation;
-
public ArrayList vertices;
public ArrayList faces;
public Mesh3D() {
- translation = new Vector3f();
vertices = new ArrayList();
faces = new ArrayList();
}
- public void apply(IMeshModifier modifier) {
- modifier.modify(this);
- }
-
- 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());
- faceNormal.addLocal(x, y, z);
- }
- return faceNormal.normalize();
+ /**
+ * 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
+ */
+ public Mesh3D apply(IMeshModifier modifier) {
+ return modifier.modify(this);
}
+ /**
+ * Rotates the mesh around the Y-axis.
+ *
+ * @deprecated Use {@link RotateYModifier} instead.
+ */
public Mesh3D rotateY(float angle) {
- Matrix3f m = new Matrix3f(Mathf.cos(angle), 0, Mathf.sin(angle), 0, 1, 0, -Mathf.sin(angle), 0,
- Mathf.cos(angle));
-
- for (Vector3f v : vertices) {
- Vector3f v0 = v.mult(m);
- v.set(v0.getX(), v.getY(), v0.getZ());
- }
-
- return this;
+ return new RotateYModifier(angle).modify(this);
}
+ /**
+ * Rotates the mesh around the Z-axis.
+ *
+ * @deprecated Use {@link RotateZModifier} instead.
+ */
public Mesh3D rotateZ(float angle) {
- Matrix3f m = new Matrix3f(Mathf.cos(angle), -Mathf.sin(angle), 0, Mathf.sin(angle), Mathf.cos(angle), 0, 0, 0,
- 1);
-
- for (Vector3f v : vertices) {
- Vector3f v0 = v.mult(m);
- v.set(v0.getX(), v0.getY(), v.getZ());
- }
-
- return this;
+ return new RotateZModifier(angle).modify(this);
}
+ /**
+ * Translates the mesh along the X-axis.
+ *
+ * @deprecated Use {@link TranslateModifier} instead.
+ */
+ @Deprecated
public Mesh3D translateX(float tx) {
- for (Vector3f v : vertices) {
- v.addLocal(tx, 0, 0);
- }
- return this;
+ return new TranslateModifier(tx, 0, 0).modify(this);
}
+ /**
+ * Translates the mesh along the Y-axis.
+ *
+ * @deprecated Use {@link TranslateModifier} instead.
+ */
public Mesh3D translateY(float ty) {
- for (Vector3f v : vertices) {
- v.addLocal(0, ty, 0);
- }
- return this;
+ return new TranslateModifier(0, ty, 0).modify(this);
}
+ /**
+ * Translates the mesh along the Z-axis.
+ *
+ * @deprecated Use {@link TranslateModifier} instead.
+ */
public Mesh3D translateZ(float tz) {
- for (Vector3f v : vertices) {
- v.addLocal(0, 0, tz);
- }
- return this;
- }
-
+ return new TranslateModifier(0, 0, tz).modify(this);
+ }
+
+ /**
+ * 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.
+ *
+ *
+ * @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();
+ Vector3f min = new Vector3f(vertices.get(0));
+ Vector3f max = new Vector3f(vertices.get(0));
+
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);
+ min.setX(Math.min(min.getX(), v.getX()));
+ min.setY(Math.min(min.getY(), v.getY()));
+ min.setZ(Math.min(min.getZ(), v.getZ()));
+
+ max.setX(Math.max(max.getX(), v.getX()));
+ max.setY(Math.max(max.getY(), v.getY()));
+ max.setZ(Math.max(max.getZ(), v.getZ()));
+ }
+
+ return new Bounds3(min, max);
+ }
+
+ 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());
+ faceNormal.addLocal(x, y, z);
}
- bounds.setMinMax(min, max);
- return bounds;
+ return faceNormal.normalize();
+ }
+
+ public void removeDoubles(int decimalPlaces) {
+ for (Vector3f v : vertices)
+ v.roundLocalDecimalPlaces(decimalPlaces);
+ removeDoubles();
+ }
+
+ /**
+ * Removes duplicated vertices.
+ *
+ * @deprecated Use {@link RemoveDoubleVerticesModifier} instead.
+ */
+ public void removeDoubles() {
+ new RemoveDoubleVerticesModifier().modify(this);
}
public Mesh3D copy() {
@@ -123,38 +156,6 @@ public Mesh3D copy() {
return copy;
}
- private Mesh3D appendUtil(Mesh3D... meshes) {
- // FIXME copy vertices and faces
- int n = 0;
- Mesh3D mesh = new Mesh3D();
- List vertices = mesh.vertices;
- List faces = mesh.faces;
-
- for (int i = 0; i < meshes.length; i++) {
- Mesh3D m = meshes[i];
- vertices.addAll(m.vertices);
- faces.addAll(meshes[i].faces);
- for (Face3D f : meshes[i].faces) {
- for (int j = 0; j < f.indices.length; j++) {
- f.indices[j] += n;
- }
- }
- n += m.getVertexCount();
- }
-
- return mesh;
- }
-
- public void removeDoubles(int decimalPlaces) {
- for (Vector3f v : vertices)
- v.roundLocalDecimalPlaces(decimalPlaces);
- removeDoubles();
- }
-
- public void removeDoubles() {
- new RemoveDoubleVerticesModifier().modify(this);
- }
-
public Vector3f calculateFaceCenter(Face3D face) {
Vector3f center = new Vector3f();
for (int i = 0; i < face.indices.length; i++) {
@@ -178,6 +179,28 @@ public Mesh3D append(Mesh3D... meshes) {
return this;
}
+ private Mesh3D appendUtil(Mesh3D... meshes) {
+ // FIXME copy vertices and faces
+ int n = 0;
+ Mesh3D mesh = new Mesh3D();
+ List vertices = mesh.vertices;
+ List faces = mesh.faces;
+
+ for (int i = 0; i < meshes.length; i++) {
+ Mesh3D m = meshes[i];
+ vertices.addAll(m.vertices);
+ faces.addAll(meshes[i].faces);
+ for (Face3D f : meshes[i].faces) {
+ for (int j = 0; j < f.indices.length; j++) {
+ f.indices[j] += n;
+ }
+ }
+ n += m.getVertexCount();
+ }
+
+ return mesh;
+ }
+
public void clearVertices() {
vertices.clear();
}
diff --git a/src/main/java/mesh/creator/assets/FloorPatternCreator.java b/src/main/java/mesh/creator/assets/FloorPatternCreator.java
index 525b58bc..4f6c9458 100644
--- a/src/main/java/mesh/creator/assets/FloorPatternCreator.java
+++ b/src/main/java/mesh/creator/assets/FloorPatternCreator.java
@@ -9,87 +9,86 @@
public class FloorPatternCreator implements IMeshCreator {
- private float height;
-
- private float radius;
-
- private int subdivisions;
-
- private Mesh3D mesh;
-
- private FaceSelection faceSelection;
-
- public FloorPatternCreator() {
- this(0.2f, 2, 4);
- }
-
- public FloorPatternCreator(float height, float radius, int subdivisions) {
- this.height = height;
- this.radius = radius;
- this.subdivisions = subdivisions;
- }
-
- @Override
- public Mesh3D create() {
- createGrid();
- initializeFaceSelection();
- selectAllFaces();
- solidify();
- extrude();
- snapToGround();
- return mesh;
- }
-
- private void snapToGround() {
- mesh.translateY(-height * 0.5f);
- }
-
- private void extrude() {
- ExtrudeModifier extrude = new ExtrudeModifier();
- extrude.setScale(0.9f);
- extrude.setAmount(height * 0.5f);
- extrude.setFacesToExtrude(faceSelection.getFaces());
- extrude.modify(mesh);
- }
-
- private void solidify() {
- new SolidifyModifier(height * 0.5f).modify(mesh);
- }
-
- private void initializeFaceSelection() {
- faceSelection = new FaceSelection(mesh);
- }
-
- private void selectAllFaces() {
- faceSelection.selectAll();
- }
-
- private void createGrid() {
- mesh = new GridCreator(subdivisions, subdivisions, radius).create();
- }
-
- public float getHeight() {
- return height;
- }
-
- public void setHeight(float height) {
- this.height = height;
- }
-
- public float getRadius() {
- return radius;
- }
-
- public void setRadius(float radius) {
- this.radius = radius;
- }
-
- public int getSubdivisions() {
- return subdivisions;
- }
-
- public void setSubdivisions(int subdivisions) {
- this.subdivisions = subdivisions;
- }
+ private float height;
+
+ private float radius;
+
+ private int subdivisions;
+
+ private Mesh3D mesh;
+
+ private FaceSelection faceSelection;
+
+ public FloorPatternCreator() {
+ this(0.2f, 2, 4);
+ }
+
+ public FloorPatternCreator(float height, float radius, int subdivisions) {
+ this.height = height;
+ this.radius = radius;
+ this.subdivisions = subdivisions;
+ }
+
+ @Override
+ public Mesh3D create() {
+ createGrid();
+ initializeFaceSelection();
+ selectAllFaces();
+ solidify();
+ extrude();
+ snapToGround();
+ return mesh;
+ }
+
+ private void snapToGround() {
+ mesh.translateY(-height * 0.5f);
+ }
+
+ private void extrude() {
+ ExtrudeModifier extrude = new ExtrudeModifier();
+ extrude.setScale(0.9f);
+ extrude.setAmount(height * 0.5f);
+ extrude.modify(mesh, faceSelection.getFaces());
+ }
+
+ private void solidify() {
+ new SolidifyModifier(height * 0.5f).modify(mesh);
+ }
+
+ private void initializeFaceSelection() {
+ faceSelection = new FaceSelection(mesh);
+ }
+
+ private void selectAllFaces() {
+ faceSelection.selectAll();
+ }
+
+ private void createGrid() {
+ mesh = new GridCreator(subdivisions, subdivisions, radius).create();
+ }
+
+ public float getHeight() {
+ return height;
+ }
+
+ public void setHeight(float height) {
+ this.height = height;
+ }
+
+ public float getRadius() {
+ return radius;
+ }
+
+ public void setRadius(float radius) {
+ this.radius = radius;
+ }
+
+ public int getSubdivisions() {
+ return subdivisions;
+ }
+
+ public void setSubdivisions(int subdivisions) {
+ this.subdivisions = subdivisions;
+ }
}
diff --git a/src/main/java/mesh/creator/creative/CubicLatticeCreator.java b/src/main/java/mesh/creator/creative/CubicLatticeCreator.java
index 2a47980b..9d1385e6 100644
--- a/src/main/java/mesh/creator/creative/CubicLatticeCreator.java
+++ b/src/main/java/mesh/creator/creative/CubicLatticeCreator.java
@@ -1,18 +1,15 @@
package mesh.creator.creative;
-import java.util.List;
-
import math.Vector3f;
-import mesh.Face3D;
import mesh.Mesh3D;
import mesh.creator.IMeshCreator;
import mesh.creator.primitives.CubeCreator;
import mesh.modifier.CenterAtModifier;
+import mesh.modifier.ExtrudeModifier;
import mesh.modifier.SolidifyModifier;
import mesh.modifier.TranslateModifier;
import mesh.modifier.subdivision.CatmullClarkModifier;
import mesh.selection.FaceSelection;
-import mesh.util.Mesh3DUtil;
public class CubicLatticeCreator implements IMeshCreator {
@@ -47,10 +44,11 @@ public Mesh3D create() {
private Mesh3D createSegment() {
Mesh3D mesh = new CubeCreator().create();
- List faces = mesh.getFaces();
- for (Face3D face : faces)
- Mesh3DUtil.extrudeFace(mesh, face, 1.0f, 0.5f);
- mesh.faces.removeAll(faces);
+ ExtrudeModifier modifier = new ExtrudeModifier();
+ modifier.setScale(1.0f);
+ modifier.setAmount(0.5f);
+ modifier.setRemoveFaces(true);
+ modifier.modify(mesh);
return mesh;
}
diff --git a/src/main/java/mesh/modifier/BendModifier.java b/src/main/java/mesh/modifier/BendModifier.java
index 818394e1..9d00aa00 100644
--- a/src/main/java/mesh/modifier/BendModifier.java
+++ b/src/main/java/mesh/modifier/BendModifier.java
@@ -5,22 +5,93 @@
import mesh.Mesh3D;
/**
- * A modifier that bends a mesh along the X-axis.
+ * A modifier that bends a 3D mesh along the X-axis.
*
* This modifier applies a simple bending deformation to each vertex of the
- * mesh. The degree of bending is controlled by the `factor` parameter. A higher
- * factor results in a more pronounced bend.
+ * given 3D mesh. The degree of bending is controlled by the {@code factor}
+ * parameter. A higher factor results in a more pronounced bend. Extreme bending
+ * may distort or cause self-intersection depending on the value of the factor.
*/
public class BendModifier implements IMeshModifier {
+ /**
+ * A very small value used to determine if the bending factor is effectively
+ * zero.
+ */
private static final float EPSILON = 1e-7f;
+ /**
+ * The bending factor determining the degree of bending along the X-axis.
+ * Default value is 0.5.
+ */
private float factor;
+ /**
+ * A modifier that bends a mesh along the X-axis.
+ *
+ * The bending is controlled by the `factor` parameter. Default is 0.5f,
+ * introducing a subtle bend.
+ */
+ public BendModifier() {
+ this(0.5f);
+ }
+
+ /**
+ * Constructor to specify a custom bending factor.
+ *
+ * @param factor the bending factor controlling the degree of bending. Higher
+ * values cause more bending, but extreme values can distort the
+ * mesh.
+ */
public BendModifier(float factor) {
this.factor = factor;
}
+ /**
+ * Modifies the provided mesh by applying bending to its vertices along the
+ * X-axis. If the provided mesh contains no vertices, the method safely returns
+ * the mesh without changes.
+ *
+ * The bending is only applied if the {@link #factor} value is valid (greater
+ * than a small threshold, defined by {@link #EPSILON}). This prevents the mesh
+ * from being unnecessarily modified when the bending factor is negligible and
+ * would result in division by zero issues.
+ *
+ *
+ * @param mesh the 3D mesh to bend. Cannot be {@code null}.
+ * @return the modified mesh after applying bending, or the original mesh if no
+ * changes are applied.
+ * @throws IllegalArgumentException if {@code mesh} is null.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ if (isFactorValid())
+ bend(mesh);
+ return mesh;
+ }
+
+ /**
+ * Performs the bending operation on all vertices of the provided mesh using
+ * parallel processing.
+ *
+ * @param mesh the 3D mesh whose vertices are to be deformed.
+ */
+ private void bend(Mesh3D mesh) {
+ mesh.vertices.parallelStream().forEach(this::simpleDeformBend);
+ }
+
+ /**
+ * Applies the bending transformation to a single vertex based on the bending
+ * equation.
+ *
+ * @param v the vertex to deform.
+ */
private void simpleDeformBend(Vector3f v) {
float theta = v.x * factor;
float sinTheta = Mathf.sin(theta);
@@ -29,30 +100,35 @@ private void simpleDeformBend(Vector3f v) {
float bx = -(v.y - 1.0f / factor) * sinTheta;
float by = (v.y - 1.0f / factor) * cosTheta + 1.0f / factor;
float bz = v.z;
-
+
v.set(bx, by, bz);
}
-
- private void bend(Mesh3D mesh) {
- for (Vector3f v : mesh.vertices)
- simpleDeformBend(v);
- }
-
+
+ /**
+ * Checks if the bending factor is valid (i.e., not effectively zero).
+ *
+ * @return {@code true} if the factor is a valid number for bending,
+ * {@code false} otherwise.
+ */
private boolean isFactorValid() {
return Mathf.abs(factor) > EPSILON;
}
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- if (isFactorValid())
- bend(mesh);
- return mesh;
- }
-
+ /**
+ * Gets the current bending factor value.
+ *
+ * @return the bending factor value.
+ */
public float getFactor() {
return factor;
}
+ /**
+ * Sets the bending factor to a new value.
+ *
+ * @param factor the new bending factor value. Higher values result in more
+ * bending.
+ */
public void setFactor(float factor) {
this.factor = factor;
}
diff --git a/src/main/java/mesh/modifier/BevelEdgesModifier.java b/src/main/java/mesh/modifier/BevelEdgesModifier.java
index 11fdd32b..acb90feb 100644
--- a/src/main/java/mesh/modifier/BevelEdgesModifier.java
+++ b/src/main/java/mesh/modifier/BevelEdgesModifier.java
@@ -15,235 +15,243 @@
public class BevelEdgesModifier implements IMeshModifier {
- private float inset;
-
- private float amount;
-
- private WidthType widthType = WidthType.OFFSET;
-
- private Mesh3D mesh;
-
- private List facesToAdd;
-
- private List verticesToAdd;
-
- private HashMap oldEdgeToNewEdge;
-
- private HashSet processed;
-
- public BevelEdgesModifier(float amount) {
- setAmount(amount);
- processed = new HashSet<>();
- oldEdgeToNewEdge = new HashMap<>();
- verticesToAdd = new ArrayList();
- facesToAdd = new ArrayList();
- }
-
- public BevelEdgesModifier() {
- this(0.1f);
- }
-
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- if (amount == 0)
- 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);
- }
- }
-
- private void createFacesVertex() {
- TraverseHelper helper = new TraverseHelper(mesh);
- for (int i = 0; i < mesh.getVertexCount(); i++) {
- Edge3D outgoingEdge = helper.getOutgoing(i);
- Edge3D edge = outgoingEdge;
- List indices = new ArrayList();
- do {
- Edge3D newEdge = oldEdgeToNewEdge.get(edge);
- int index = newEdge.fromIndex;
- indices.add(index);
- edge = helper.getPairNext(edge.fromIndex, edge.toIndex);
- } while (!outgoingEdge.equals(edge));
- facesToAdd.add(new Face3D(toReverseArray(indices)));
- }
- }
-
- private void createFacesForOldEdges() {
- for (Face3D face : mesh.faces)
- for (int i = 0; i < face.indices.length; i++)
- createFaceForOldEdgeAt(face, i);
- }
-
- private void createFaceForOldEdgeAt(Face3D face, int i) {
- Edge3D edge = getMappedEdge(createEdgeAt(face.indices, i));
- Edge3D pair = getMappedEdge(createEdgeAt(face.indices, i).createPair());
-
- if (isProcessed(edge) || isProcessed(pair))
- return;
-
- addNewFace(new int[] { edge.toIndex, edge.fromIndex, pair.toIndex,
- pair.fromIndex });
-
- markAsProcessed(edge);
- markAsProcessed(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 Edge3D getMappedEdge(Edge3D edge) {
- return oldEdgeToNewEdge.get(edge);
- }
-
- private Edge3D createEdgeAt(int[] indices, int i) {
- return new Edge3D(indices[i], indices[(i + 1) % indices.length]);
- }
-
- private int[] createIndices(int size, int nextVertexIndex) {
- int[] indices = new int[size];
- for (int i = 0; i < indices.length; i++)
- indices[i] = nextVertexIndex + i;
- return indices;
- }
-
- private int[] toReverseArray(List values) {
- return values.stream().sorted(Collections.reverseOrder())
- .mapToInt(x -> x).toArray();
- }
-
- private void clearAll() {
- processed.clear();
- oldEdgeToNewEdge.clear();
- verticesToAdd.clear();
- facesToAdd.clear();
- }
-
- private void markAsProcessed(Edge3D edge) {
- processed.add(edge);
- }
-
- private boolean isProcessed(Edge3D edge) {
- return processed.contains(edge);
- }
-
- private Vector3f getVertexAt(Face3D face, int index) {
- return mesh.getVertexAt(face.indices[index % face.indices.length]);
- }
-
- private void addNewVertices() {
- mesh.vertices.addAll(verticesToAdd);
- }
-
- private void addNewFaces() {
- mesh.faces.addAll(facesToAdd);
- }
-
- private void clearOriginalVertices() {
- mesh.vertices.clear();
- }
-
- private void clearOriginalFaces() {
- mesh.faces.clear();
- }
-
- private void addNewFace(int[] indices) {
- facesToAdd.add(new Face3D(indices));
- }
-
- private void setMesh(Mesh3D mesh) {
- this.mesh = mesh;
- }
-
- public void setAmount(float amount) {
- this.amount = amount;
- updateInset();
- }
-
- private void updateInset() {
- inset = Mathf.sqrt((amount * amount) + (amount * amount));
- }
-
- public WidthType getWidthType() {
- return widthType;
- }
-
- public void setWidthType(WidthType widthType) {
- this.widthType = widthType;
- }
+ public enum WidthType {
+
+ OFFSET,
+
+ WIDTH,
+
+ DEPTH,
+
+ }
+
+ private float inset;
+
+ private float amount;
+
+ private WidthType widthType = WidthType.OFFSET;
+
+ private Mesh3D mesh;
+
+ private List facesToAdd;
+
+ private List verticesToAdd;
+
+ private HashMap oldEdgeToNewEdge;
+
+ private HashSet processed;
+
+ public BevelEdgesModifier(float amount) {
+ setAmount(amount);
+ processed = new HashSet<>();
+ oldEdgeToNewEdge = new HashMap<>();
+ verticesToAdd = new ArrayList();
+ facesToAdd = new ArrayList();
+ }
+
+ public BevelEdgesModifier() {
+ this(0.1f);
+ }
+
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (amount == 0)
+ 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);
+ }
+ }
+
+ private void createFacesVertex() {
+ TraverseHelper helper = new TraverseHelper(mesh);
+ for (int i = 0; i < mesh.getVertexCount(); i++) {
+ Edge3D outgoingEdge = helper.getOutgoing(i);
+ Edge3D edge = outgoingEdge;
+ List indices = new ArrayList();
+ do {
+ Edge3D newEdge = oldEdgeToNewEdge.get(edge);
+ int index = newEdge.fromIndex;
+ indices.add(index);
+ edge = helper.getPairNext(edge.fromIndex, edge.toIndex);
+ } while (!outgoingEdge.equals(edge));
+ facesToAdd.add(new Face3D(toReverseArray(indices)));
+ }
+ }
+
+ private void createFacesForOldEdges() {
+ for (Face3D face : mesh.faces)
+ for (int i = 0; i < face.indices.length; i++)
+ createFaceForOldEdgeAt(face, i);
+ }
+
+ private void createFaceForOldEdgeAt(Face3D face, int i) {
+ Edge3D edge = getMappedEdge(createEdgeAt(face.indices, i));
+ Edge3D pair = getMappedEdge(createEdgeAt(face.indices, i).createPair());
+
+ if (isProcessed(edge) || isProcessed(pair))
+ return;
+
+ addNewFace(new int[] { edge.toIndex, edge.fromIndex, pair.toIndex, pair.fromIndex });
+
+ markAsProcessed(edge);
+ markAsProcessed(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 Edge3D getMappedEdge(Edge3D edge) {
+ return oldEdgeToNewEdge.get(edge);
+ }
+
+ private Edge3D createEdgeAt(int[] indices, int i) {
+ return new Edge3D(indices[i], indices[(i + 1) % indices.length]);
+ }
+
+ private int[] createIndices(int size, int nextVertexIndex) {
+ int[] indices = new int[size];
+ for (int i = 0; i < indices.length; i++)
+ indices[i] = nextVertexIndex + i;
+ return indices;
+ }
+
+ private int[] toReverseArray(List values) {
+ return values.stream().sorted(Collections.reverseOrder()).mapToInt(x -> x).toArray();
+ }
+
+ private void clearAll() {
+ processed.clear();
+ oldEdgeToNewEdge.clear();
+ verticesToAdd.clear();
+ facesToAdd.clear();
+ }
+
+ private void markAsProcessed(Edge3D edge) {
+ processed.add(edge);
+ }
+
+ private boolean isProcessed(Edge3D edge) {
+ return processed.contains(edge);
+ }
+
+ private Vector3f getVertexAt(Face3D face, int index) {
+ return mesh.getVertexAt(face.indices[index % face.indices.length]);
+ }
+
+ private void addNewVertices() {
+ mesh.vertices.addAll(verticesToAdd);
+ }
+
+ private void addNewFaces() {
+ mesh.faces.addAll(facesToAdd);
+ }
+
+ private void clearOriginalVertices() {
+ mesh.vertices.clear();
+ }
+
+ private void clearOriginalFaces() {
+ mesh.faces.clear();
+ }
+
+ private void addNewFace(int[] indices) {
+ facesToAdd.add(new Face3D(indices));
+ }
+
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ public void setAmount(float amount) {
+ this.amount = amount;
+ updateInset();
+ }
+
+ private void updateInset() {
+ inset = Mathf.sqrt((amount * amount) + (amount * amount));
+ }
+
+ public WidthType getWidthType() {
+ return widthType;
+ }
+
+ public void setWidthType(WidthType widthType) {
+ this.widthType = widthType;
+ }
}
diff --git a/src/main/java/mesh/modifier/CenterAtModifier.java b/src/main/java/mesh/modifier/CenterAtModifier.java
index a30c3e54..3a65c72b 100644
--- a/src/main/java/mesh/modifier/CenterAtModifier.java
+++ b/src/main/java/mesh/modifier/CenterAtModifier.java
@@ -4,34 +4,135 @@
import mesh.Mesh3D;
import mesh.util.Bounds3;
+/**
+ * A modifier that centers a 3D mesh at a specified point in space.
+ *
+ * This class translates the mesh so that its center aligns with the given
+ * target center. If the mesh is already centered within a small threshold
+ * defined by {@code EPSILON}, no changes are made.
+ */
public class CenterAtModifier implements IMeshModifier {
- private Vector3f center;
-
- public CenterAtModifier() {
- center = new Vector3f();
- }
-
- public CenterAtModifier(Vector3f center) {
- this.center = center;
- }
-
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- Bounds3 bounds = mesh.calculateBounds();
- if (bounds.getCenter().equals(center))
- return mesh;
- Vector3f distance = center.subtract(bounds.getCenter());
- mesh.apply(new TranslateModifier(distance));
- return mesh;
- }
-
- public Vector3f getCenter() {
- return center;
- }
-
- public void setCenter(Vector3f center) {
- this.center = center;
- }
+ /**
+ * A small threshold used to determine if the mesh is already centered.
+ */
+ public static final float EPSILON = 1e-6f;
+
+ /**
+ * The target center point to which the mesh should be aligned.
+ */
+ private Vector3f center;
+
+ /**
+ * The mesh currently being modified.
+ */
+ private Mesh3D mesh;
+
+ /**
+ * The calculated bounding box of the mesh.
+ */
+ private Bounds3 bounds;
+
+ /**
+ * Constructs a new {@code CenterAtModifier} with the default center at the
+ * origin (0, 0, 0).
+ */
+ public CenterAtModifier() {
+ center = new Vector3f();
+ }
+
+ /**
+ * Constructs a new {@code CenterAtModifier} with the specified target center.
+ *
+ * @param center the target center point. Cannot be {@code null}. throws
+ * IllegalArgumentException if {@code center} is null.
+ */
+ public CenterAtModifier(Vector3f center) {
+ setCenter(center);
+ }
+
+ /**
+ * Modifies the provided mesh by translating it to align its center with the
+ * target center. If the mesh is already centered (within {@code EPSILON}), no
+ * changes are made.
+ *
+ * @param mesh the 3D mesh to center. Cannot be {@code null}.
+ * @return the modified mesh after centering, or the original mesh if no changes
+ * were applied.
+ * @throws IllegalArgumentException if {@code mesh} is null.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ calculateBounds();
+ if (meshIsAlreadyCentered()) {
+ return mesh;
+ }
+ centerMesh();
+ return mesh;
+ }
+
+ /**
+ * Translates the mesh to align its center with the target center.
+ */
+ private void centerMesh() {
+ Vector3f distance = center.subtract(bounds.getCenter());
+ mesh.apply(new TranslateModifier(distance));
+ }
+
+ /**
+ * Sets the mesh to be modified.
+ *
+ * @param mesh the mesh to modify
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Calculates the bounds of the current mesh.
+ */
+ private void calculateBounds() {
+ this.bounds = mesh.calculateBounds();
+ }
+
+ /**
+ * Checks whether the mesh is already centered at the target center within the
+ * threshold {@code EPSILON}.
+ *
+ * @return {@code true} if the mesh is already centered; {@code false}
+ * otherwise.
+ */
+ private boolean meshIsAlreadyCentered() {
+ return bounds.getCenter().distance(center) < EPSILON;
+ }
+
+ /**
+ * Returns the target center point.
+ *
+ * @return the target center point.
+ */
+ public Vector3f getCenter() {
+ return center;
+ }
+
+ /**
+ * Sets the target center point to which the mesh should be aligned.
+ *
+ * @param center the new target center point. Cannot be {@code null}.
+ * @throws IllegalArgumentException if {@code center} is null.
+ */
+ public void setCenter(Vector3f center) {
+ if (center == null) {
+ throw new IllegalArgumentException("Center cannot be null");
+ }
+ this.center = center;
+ }
}
diff --git a/src/main/java/mesh/modifier/CrocodileModifier.java b/src/main/java/mesh/modifier/CrocodileModifier.java
index d392d9b6..c1dd0045 100644
--- a/src/main/java/mesh/modifier/CrocodileModifier.java
+++ b/src/main/java/mesh/modifier/CrocodileModifier.java
@@ -1,79 +1,175 @@
package mesh.modifier;
-import java.util.ArrayList;
-import java.util.List;
-
import math.Vector3f;
import mesh.Face3D;
import mesh.Mesh3D;
import mesh.conway.ConwayAmboModifier;
import mesh.selection.FaceSelection;
+/**
+ * The `CrocodileModifier` transforms a 3D mesh by applying the Conway Ambo
+ * operation and adding spike-like protrusions to the resulting faces.
+ *
+ *
+ * - Ambo Operation: This operation slices off the corners of the original
+ * polyhedron by creating slicing planes that pass through the midpoints of
+ * edges. This produces new vertices and new polygonal faces, effectively
+ * generating a new polyhedral structure.
+ *
+ * - Spike Creation: After applying the Ambo operation, spike-like protrusions
+ * are added to the centers of these newly created faces. These spikes extend
+ * outward, giving the resulting mesh a spiky appearance. This spike effect can
+ * be viewed as a variation of the Kis operation.
+ *
+ * - Kis-Like Operation: While the traditional Kis operation modifies all faces
+ * of a polyhedron, this implementation is limited to targeting only the newly
+ * created faces resulting from the Ambo operation. This allows for a more
+ * focused and controlled deformation.
+ *
+ */
public class CrocodileModifier implements IMeshModifier {
- private float distance;
-
- private Mesh3D mesh;
-
- private FaceSelection selection;
-
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- setMesh(mesh);
- ambo();
- selectFaces();
- createSpikes();
- removeSelectedFaces();
- return mesh;
- }
-
- private void createSpikes() {
- int nextIndex = mesh.vertices.size();
- List facesToAdd = new ArrayList();
-
- for (Face3D face : selection.getFaces()) {
- Vector3f center = mesh.calculateFaceCenter(face);
- Vector3f normal = mesh.calculateFaceNormal(face);
- for (int i = 0; i < face.indices.length; i++) {
- int fromIndex = face.indices[i];
- int toIndex = face.indices[(i + 1) % face.indices.length];
- int centerIndex = nextIndex;
- Face3D newTriangle = new Face3D(fromIndex, toIndex,
- centerIndex);
- newTriangle.tag = "spikes";
- facesToAdd.add(newTriangle);
- }
- center.addLocal(normal.mult(distance));
- mesh.add(center);
- nextIndex++;
- }
-
- mesh.faces.addAll(facesToAdd);
- }
-
- private void ambo() {
- new ConwayAmboModifier().modify(mesh);
- }
-
- private void selectFaces() {
- selection = new FaceSelection(mesh);
- selection.selectByTag("ambo");
- }
-
- private void removeSelectedFaces() {
- mesh.faces.removeAll(selection.getFaces());
- }
-
- private void setMesh(Mesh3D mesh) {
- this.mesh = mesh;
- }
-
- public float getDistance() {
- return distance;
- }
-
- public void setDistance(float distance) {
- this.distance = distance;
- }
+ /**
+ * The distance defines how much the spike protrudes outward from the face's
+ * center.
+ */
+ private float distance;
+
+ /** The 3D mesh being modified. */
+ private Mesh3D mesh;
+
+ /** Selection utility to track faces created by the Ambo operation. */
+ private FaceSelection selection;
+
+ /**
+ * Modifies the given 3D mesh by applying the Ambo operation, creating spikes,
+ * and removing certain faces to produce the final transformation.
+ *
+ * @param mesh The 3D mesh to be transformed.
+ * @return The modified 3D mesh.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ validateMesh(mesh);
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ applConwayAmboOperation();
+ selectAmboFaces();
+ createSpikes();
+ removeSelectedAmboFaces();
+ return mesh;
+ }
+
+ /**
+ * Creates spike-like protrusions by generating new triangular faces at the
+ * centers of selected faces.
+ */
+ private void createSpikes() {
+ int nextIndex = mesh.vertices.size();
+ for (Face3D face : selection.getFaces()) {
+ mesh.add(calculateSpikeTip(face));
+ createSpikeFaces(face, nextIndex);
+ nextIndex++;
+ }
+ }
+
+ /**
+ * Calculates the tip of the spike based on the center of the given face and
+ * projects it outward along the normal vector scaled by the distance.
+ *
+ * @param face The face used to calculate the spike tip.
+ * @return The 3D position of the spike tip.
+ */
+ private Vector3f calculateSpikeTip(Face3D face) {
+ Vector3f center = mesh.calculateFaceCenter(face);
+ Vector3f normal = mesh.calculateFaceNormal(face);
+ if (distance == 0) {
+ return center;
+ }
+ return center.add(normal.mult(distance));
+ }
+
+ /**
+ * Generates the triangular spike faces connecting edges of a given face to the
+ * spike tip.
+ *
+ * @param face The target face to create spikes for.
+ * @param nextIndex The index of the newly created spike tip vertex.
+ */
+ private void createSpikeFaces(Face3D face, int nextIndex) {
+ for (int i = 0; i < face.indices.length; i++) {
+ int fromIndex = face.indices[i];
+ int toIndex = face.indices[(i + 1) % face.indices.length];
+ int centerIndex = nextIndex;
+ Face3D triangle = new Face3D(fromIndex, toIndex, centerIndex);
+ triangle.tag = "spikes";
+ mesh.add(triangle);
+ }
+ }
+
+ /**
+ * Validates that the given mesh is not null.
+ *
+ * @param mesh The mesh to validate.
+ */
+ private void validateMesh(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ }
+
+ /**
+ * Applies the Conway Ambo operation to the 3D mesh to generate new faces and
+ * vertices.
+ */
+ private void applConwayAmboOperation() {
+ new ConwayAmboModifier().modify(mesh);
+ }
+
+ /**
+ * Selects the faces created by the Ambo operation using their assigned tags.
+ */
+ private void selectAmboFaces() {
+ selection = new FaceSelection(mesh);
+ selection.selectByTag("ambo");
+ }
+
+ /**
+ * Removes all the newly created faces (tagged by the Ambo operation) from the
+ * mesh as part of the transformation process.
+ */
+ private void removeSelectedAmboFaces() {
+ mesh.faces.removeAll(selection.getFaces());
+ }
+
+ /**
+ * Sets the current mesh for transformation operations.
+ *
+ * @param mesh The 3D mesh to operate on.
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Retrieves the distance value, which defines how far spikes protrude.
+ *
+ * @return The current distance value.
+ */
+ public float getDistance() {
+ return distance;
+ }
+
+ /**
+ * Sets the distance value, which controls how far the spike-like protrusions
+ * are displaced from their originating face centers.
+ *
+ * @param distance The distance value to set.
+ */
+ public void setDistance(float distance) {
+ this.distance = distance;
+ }
}
diff --git a/src/main/java/mesh/modifier/ExtrudeModifier.java b/src/main/java/mesh/modifier/ExtrudeModifier.java
index 30e03428..25d86e64 100644
--- a/src/main/java/mesh/modifier/ExtrudeModifier.java
+++ b/src/main/java/mesh/modifier/ExtrudeModifier.java
@@ -2,11 +2,31 @@
import java.util.Collection;
+import math.Vector3f;
import mesh.Face3D;
import mesh.Mesh3D;
-import mesh.util.Mesh3DUtil;
-public class ExtrudeModifier implements IMeshModifier {
+/**
+ * The {@code ExtrudeModifier} class implements the functionality of extruding
+ * faces of a 3D mesh by a given amount and scaling factor. It allows faces to
+ * be extruded outward, inward, and optionally removed from the mesh after
+ * extrusion. This class implements {@code IMeshModifier} and
+ * {@code FaceModifier} interfaces.
+ *
+ *
+ * Key features:
+ *
+ * - Extrusion of a specified collection of faces, or individual faces.
+ * - Adjustable scaling and extrusion amount parameters.
+ * - Optionally remove faces after extrusion using the {@code removeFaces}
+ * flag.
+ *
+ */
+public class ExtrudeModifier implements IMeshModifier, FaceModifier {
+
+ private static final float DEFAULT_SCALE = 1.0f;
+
+ private static final float DEFAULT_AMOUNT = 0.0f;
private boolean removeFaces;
@@ -14,61 +34,211 @@ public class ExtrudeModifier implements IMeshModifier {
private float amount;
- private Collection faces;
-
+ /**
+ * Default constructor initializing with default scale and amount values. Scale
+ * = 1.0, Amount = 0.0.
+ */
public ExtrudeModifier() {
-
+ this(DEFAULT_SCALE, DEFAULT_AMOUNT);
}
+ /**
+ * Constructor initializing the ExtrudeModifier with a specified scale and
+ * amount.
+ *
+ * @param scale the scaling factor to apply to the extruded geometry (must be
+ * >= 0).
+ * @param amount the distance to extrude faces by. A positive value extrudes
+ * outward; a negative value extrudes inward.
+ */
public ExtrudeModifier(float scale, float amount) {
+ validateScale(scale);
this.scale = scale;
this.amount = amount;
}
+ /**
+ * Modifies the entire 3D mesh by extruding all faces in the mesh by the current
+ * scale and amount.
+ *
+ * @param mesh the 3D mesh to modify.
+ * @return the modified mesh.
+ */
+ @Override
public Mesh3D modify(Mesh3D mesh) {
- if (faces == null)
- faces = mesh.getFaces();
- modify(mesh, faces);
- return mesh;
+ validateMesh(mesh);
+ if (mesh.faces.isEmpty()) {
+ return mesh;
+ }
+ return modify(mesh, mesh.getFaces());
}
- public void modify(Mesh3D mesh, Collection faces) {
+ /**
+ * Modifies a 3D mesh by extruding a specific set of faces.
+ *
+ * @param mesh the 3D mesh to modify.
+ * @param faces the collection of faces to extrude.
+ * @return the modified mesh.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh, Collection faces) {
+ validateMesh(mesh);
+ validateFaces(faces);
+ if (faces.isEmpty()) {
+ return mesh;
+ }
for (Face3D face : faces)
- Mesh3DUtil.extrudeFace(mesh, face, scale, amount);
+ extrudeFace(mesh, face);
if (removeFaces)
mesh.faces.removeAll(faces);
+ return mesh;
}
- public void modify(Mesh3D mesh, Face3D face) {
- Mesh3DUtil.extrudeFace(mesh, face, scale, amount);
+ /**
+ * Modifies the mesh by extruding a single face.
+ *
+ * @param mesh the 3D mesh to modify.
+ * @param face the single face to extrude.
+ * @return the modified mesh.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh, Face3D face) {
+ validateMesh(mesh);
+ if (face == null) {
+ throw new IllegalArgumentException("Face cannot be null.");
+ }
+ extrudeFace(mesh, face);
if (removeFaces)
mesh.removeFace(face);
+ return mesh;
+ }
+
+ /**
+ * Validates if the provided mesh is not null.
+ *
+ * @param mesh the 3D mesh to validate.
+ */
+ private void validateMesh(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ }
+
+ /**
+ * Validates if the provided collection of faces is not null.
+ *
+ * @param faces the faces to validate.
+ */
+ private void validateFaces(Collection faces) {
+ if (faces == null) {
+ throw new IllegalArgumentException("Faces cannot be null.");
+ }
+ }
+
+ /**
+ * Validates that the scale is valid (>= 0).
+ *
+ * @param scale the scaling factor to validate.
+ */
+ private void validateScale(float scale) {
+ if (scale < 0)
+ throw new IllegalArgumentException("Scale must be greater than or equal to 0.");
+ }
+
+ /**
+ * Executes the extrusion logic on the given face.
+ *
+ * @param mesh the 3D mesh to modify.
+ * @param face the face to extrude.
+ */
+ private void extrudeFace(Mesh3D mesh, Face3D face) {
+ int n = face.indices.length;
+ int nextIndex = mesh.vertices.size();
+ Vector3f normal = mesh.calculateFaceNormal(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));
+ mesh.add(vertex);
+ mesh.addFace(face.indices[i], face.indices[(i + 1) % n], nextIndex + ((i + 1) % n), nextIndex + i);
+ }
+
+ updateFaceIndices(face, nextIndex);
+ }
+
+ /**
+ * Updates the indices of the given face after extrusion to point to the newly
+ * created vertices in the mesh's vertex list. This is necessary because new
+ * vertices are added during the extrusion process, and their indices must be
+ * correctly reflected in the face indices to maintain the mesh's structural
+ * integrity.
+ *
+ * @param face the {@link Face3D} object whose indices need to be updated.
+ * @param nextIndex the starting index of the newly added vertices in the mesh's
+ * vertex list.
+ */
+ private void updateFaceIndices(Face3D face, int nextIndex) {
+ for (int i = 0; i < face.indices.length; i++) {
+ face.indices[i] = nextIndex + i;
+ }
}
+ /**
+ * Gets the current scaling factor applied during extrusion.
+ *
+ * @return the current scale.
+ */
public float getScale() {
return scale;
}
+ /**
+ * Sets the scaling factor for the extrusion.
+ *
+ * @param scale the scaling factor to set (must be >= 0).
+ */
public void setScale(float scale) {
+ validateScale(scale);
this.scale = scale;
}
+ /**
+ * Gets the amount of extrusion currently configured.
+ *
+ * @return the amount of extrusion.
+ */
public float getAmount() {
return amount;
}
+ /**
+ * Sets the amount to extrude faces by.
+ *
+ * @param amount the distance to extrude. Positive values extrude outward,
+ * negative values extrude inward.
+ */
public void setAmount(float amount) {
this.amount = amount;
}
- public void setFacesToExtrude(Collection faces) {
- this.faces = faces;
- }
-
+ /**
+ * Checks whether faces will be removed after extrusion.
+ *
+ * @return true if faces should be removed after extrusion, false otherwise.
+ */
public boolean isRemoveFaces() {
return removeFaces;
}
+ /**
+ * Sets whether faces should be removed from the mesh after extrusion.
+ *
+ * @param removeFaces if true, faces will be removed after extrusion.
+ */
public void setRemoveFaces(boolean removeFaces) {
this.removeFaces = removeFaces;
}
diff --git a/src/main/java/mesh/modifier/FaceModifier.java b/src/main/java/mesh/modifier/FaceModifier.java
index 11e4343a..6d5cd78c 100644
--- a/src/main/java/mesh/modifier/FaceModifier.java
+++ b/src/main/java/mesh/modifier/FaceModifier.java
@@ -5,10 +5,55 @@
import mesh.Face3D;
import mesh.Mesh3D;
+/**
+ * The {@code FaceModifier} interface defines a contract for operations that can
+ * modify a 3D mesh by manipulating specific faces or collections of faces.
+ * Implementations of this interface should provide logic for modifying
+ * individual or groups of faces within a 3D mesh.
+ *
+ *
+ * Key methods:
+ *
+ * - {@code modify(Mesh3D mesh, Face3D face)} - Modifies a single face in the
+ * provided 3D mesh.
+ * - {@code modify(Mesh3D mesh, Collection faces)} - Modifies a group
+ * of faces in the provided 3D mesh.
+ *
+ *
+ *
+ *
+ * Implementations of this interface should ensure that face-specific operations
+ * are isolated to avoid unintended side-effects on adjacent or connected faces
+ * unless explicitly intended. For instance, operations like edge splitting or
+ * other transformations on a single face could affect surrounding faces, as
+ * their topology might be interconnected. These effects must be considered when
+ * implementing or using a {@code FaceModifier} with mesh modifiers.
+ *
+ *
+ * Implementations of this interface can encapsulate operations such as
+ * extrusion, scaling, transformation, or other face-specific geometry changes.
+ * Caution should be taken with face operations that alter the underlying mesh
+ * structure to avoid breaking mesh integrity unless such changes are well
+ * understood and intentionally applied.
+ */
public interface FaceModifier {
-
+
+ /**
+ * Modifies a single face within the provided 3D mesh.
+ *
+ * @param mesh the {@link Mesh3D} object to modify.
+ * @param face the single {@link Face3D} to be modified.
+ * @return the modified {@link Mesh3D} object.
+ */
Mesh3D modify(Mesh3D mesh, Face3D face);
-
+
+ /**
+ * Modifies a collection of faces within the provided 3D mesh.
+ *
+ * @param mesh the {@link Mesh3D} object to modify.
+ * @param faces the collection of {@link Face3D} to be modified.
+ * @return the modified {@link Mesh3D} object.
+ */
Mesh3D modify(Mesh3D mesh, Collection faces);
-}
+}
\ No newline at end of file
diff --git a/src/main/java/mesh/modifier/FitToAABBModifier.java b/src/main/java/mesh/modifier/FitToAABBModifier.java
index 5cf00bbf..9407c2dd 100644
--- a/src/main/java/mesh/modifier/FitToAABBModifier.java
+++ b/src/main/java/mesh/modifier/FitToAABBModifier.java
@@ -1,64 +1,124 @@
package mesh.modifier;
import math.Mathf;
+import math.Vector3f;
import mesh.Mesh3D;
import mesh.util.Bounds3;
+/**
+ * A modifier that scales a 3D mesh uniformly to fit within a specified
+ * axis-aligned bounding box (AABB). The scaling ensures that the largest
+ * dimension of the mesh is scaled to match the smallest target dimension
+ * without altering the aspect ratio of the mesh.
+ */
public class FitToAABBModifier implements IMeshModifier {
- private float width;
-
- private float height;
-
- private float depth;
-
- private Mesh3D mesh;
-
- public FitToAABBModifier(float width, float height, float depth) {
- this.width = width;
- this.height = height;
- this.depth = depth;
- }
-
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- setMesh(mesh);
- scale(calculateScale());
- return mesh;
- }
-
- private void scale(float scale) {
- mesh.apply(new ScaleModifier(scale));
- }
-
- private float calculateScale() {
- float min = minTargetValue();
- float max = maxSourceValue();
- return min / max;
- }
-
- private float minTargetValue() {
- return min(width, height, depth);
- }
-
- private float maxSourceValue() {
- Bounds3 bounds = mesh.calculateBounds();
- float width = bounds.getWidth();
- float height = bounds.getHeight();
- float depth = bounds.getDepth();
- return max(width, height, depth);
- }
-
- private float min(float... values) {
- return Mathf.min(values);
- }
-
- private float max(float... values) {
- return Mathf.max(values);
- }
-
- private void setMesh(Mesh3D mesh) {
- this.mesh = mesh;
- }
+ /**
+ * The target dimensions of the AABB to fit the mesh into. The x, y, and z
+ * values must be greater than zero.
+ */
+ private Vector3f targetDimension;
+
+ /**
+ * The mesh to be modified.
+ */
+ private Mesh3D mesh;
+
+ /**
+ * Creates a new modifier with the specified target AABB dimensions.
+ *
+ * @param width the target width of the AABB; must be greater than zero.
+ * @param height the target height of the AABB; must be greater than zero.
+ * @param depth the target depth of the AABB; must be greater than zero.
+ * @throws IllegalArgumentException if any dimension is less than or equal to
+ * zero.
+ */
+ public FitToAABBModifier(float width, float height, float depth) {
+ targetDimension = new Vector3f(width, height, depth);
+ validateTargetDimension();
+ }
+
+ /**
+ * Modifies the mesh by scaling it uniformly to fit within the target AABB. If
+ * the provided mesh contains no vertices, the method safely returns the mesh
+ * without changes.
+ *
+ * @param mesh the mesh to be scaled; cannot be null.
+ * @return the scaled mesh.
+ * @throws IllegalArgumentException if the provided mesh is null.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ applyScale(calculateScale());
+ return mesh;
+ }
+
+ /**
+ * Validates that the target dimensions are greater than zero.
+ *
+ * @throws IllegalArgumentException if any target dimension is less than or
+ * equal to zero.
+ */
+ private void validateTargetDimension() {
+ if (targetDimension.x <= 0 || targetDimension.y <= 0 || targetDimension.z <= 0) {
+ throw new IllegalArgumentException("Target dimensions must be greater than zero.");
+ }
+ }
+
+ /**
+ * Applies a uniform scale to the mesh.
+ *
+ * @param scale the scale factor to apply.
+ */
+ private void applyScale(float scale) {
+ mesh.apply(new ScaleModifier(scale));
+ }
+
+ /**
+ * Calculates the uniform scaling factor based on the target dimensions and the
+ * current bounding box dimensions of the mesh.
+ *
+ * @return the calculated scale factor.
+ */
+ private float calculateScale() {
+ float minDimension = getMinimumTargetDimension();
+ float maxDimension = getMaximumSourceDimension();
+ return minDimension / (maxDimension == 0 ? 1 : maxDimension);
+ }
+
+ /**
+ * Determines the smallest dimension from the target AABB.
+ *
+ * @return the smallest target dimension.
+ */
+ private float getMinimumTargetDimension() {
+ return Mathf.min(targetDimension.x, targetDimension.y, targetDimension.z);
+ }
+
+ /**
+ * Determines the largest dimension of the current mesh bounding box.
+ *
+ * @return the largest source dimension.
+ */
+ private float getMaximumSourceDimension() {
+ Bounds3 bounds = mesh.calculateBounds();
+ return Mathf.max(bounds.getWidth(), bounds.getHeight(), bounds.getDepth());
+ }
+
+ /**
+ * Sets the current mesh being modified.
+ *
+ * @param mesh the mesh to modify.
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
}
diff --git a/src/main/java/mesh/modifier/FlipFacesModifier.java b/src/main/java/mesh/modifier/FlipFacesModifier.java
index 9a009727..925cbabb 100644
--- a/src/main/java/mesh/modifier/FlipFacesModifier.java
+++ b/src/main/java/mesh/modifier/FlipFacesModifier.java
@@ -1,37 +1,120 @@
package mesh.modifier;
-import java.util.Arrays;
import java.util.Collection;
import mesh.Face3D;
import mesh.Mesh3D;
+/**
+ * A modifier responsible for inverting the indices of 3D mesh faces. This is
+ * typically used to reverse the winding order of faces, which can impact
+ * rendering order, normal direction, or surface orientation.
+ *
+ *
+ * Note: This modifier only flips the indices of the faces. It does not account
+ * for ensuring the correct order of vertices relative to surface normals or
+ * other geometric properties. Additional adjustments may be needed for proper
+ * rendering or normal recalculations.
+ *
+ *
+ *
+ * Implements both single-face and collection-based operations to handle
+ * individual or multiple faces in a given mesh.
+ *
+ */
public class FlipFacesModifier implements IMeshModifier, FaceModifier {
+ /**
+ * Inverts the indices of all faces in the provided mesh.
+ *
+ * @param mesh The 3D mesh whose faces will be inverted.
+ * @return The modified mesh with inverted face indices.
+ */
@Override
public Mesh3D modify(Mesh3D mesh) {
- modify(mesh, mesh.faces);
- return mesh;
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ return modify(mesh, mesh.faces);
}
+ /**
+ * Inverts the indices of a single face in the provided mesh.
+ *
+ * @param mesh The 3D mesh containing the face to modify.
+ * @param face The single face whose indices will be inverted.
+ * @return The modified mesh with the single face's indices inverted.
+ */
@Override
public Mesh3D modify(Mesh3D mesh, Face3D face) {
- invertFaceIndices(mesh, face);
+ validate(mesh, face);
+ invertFaceIndices(face);
return mesh;
}
+ /**
+ * Inverts the indices of a collection of faces in the provided mesh.
+ *
+ * @param mesh The 3D mesh containing faces to modify.
+ * @param faces A collection of faces to process and invert.
+ * @return The modified mesh with all specified faces' indices inverted.
+ */
@Override
public Mesh3D modify(Mesh3D mesh, Collection faces) {
- for (Face3D face : faces)
- invertFaceIndices(mesh, face);
+ validate(mesh, faces);
+ if (faces.isEmpty()) {
+ return mesh;
+ }
+ faces.parallelStream().forEach(this::invertFaceIndices);
return mesh;
}
- private void invertFaceIndices(Mesh3D mesh, Face3D face) {
- int[] copy = Arrays.copyOf(face.indices, face.indices.length);
- for (int i = 0; i < face.indices.length; i++) {
- face.indices[i] = copy[face.indices.length - 1 - i];
+ /**
+ * Validates that the provided mesh and single face are valid and not null.
+ *
+ * @param mesh The 3D mesh to validate.
+ * @param face The single face to validate.
+ * @throws IllegalArgumentException if either the mesh or face is null.
+ */
+ private void validate(Mesh3D mesh, Face3D face) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (face == null) {
+ throw new IllegalArgumentException("Face cannot be null.");
+ }
+ }
+
+ /**
+ * Validates that the provided mesh and a collection of faces are valid and not
+ * null.
+ *
+ * @param mesh The 3D mesh to validate.
+ * @param faces The collection of faces to validate.
+ * @throws IllegalArgumentException if either the mesh or the faces collection
+ * is null.
+ */
+ private void validate(Mesh3D mesh, Collection faces) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (faces == null) {
+ throw new IllegalArgumentException("Faces collection cannot be null.");
+ }
+ }
+
+ /**
+ * Performs in-place index inversion on a single face's indices to change its
+ * winding order.
+ *
+ * @param face The face whose indices are to be inverted.
+ */
+ private void invertFaceIndices(Face3D face) {
+ for (int i = 0, j = face.indices.length - 1; i < j; i++, j--) {
+ int temp = face.indices[i];
+ face.indices[i] = face.indices[j];
+ face.indices[j] = temp;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/mesh/modifier/IMeshModifier.java b/src/main/java/mesh/modifier/IMeshModifier.java
index de6d0e8f..58b6e15e 100644
--- a/src/main/java/mesh/modifier/IMeshModifier.java
+++ b/src/main/java/mesh/modifier/IMeshModifier.java
@@ -2,8 +2,47 @@
import mesh.Mesh3D;
+/**
+ * IMeshModifier defines the interface for operations that transform or modify
+ * 3D meshes.
+ *
+ * Mesh modifiers are tools that allow for geometric transformations on 3D
+ * meshes. These transformations include operations such as translation,
+ * rotation, scaling, bending, and other geometric manipulations. The library
+ * provides a variety of pre-built modifiers that adhere to this interface,
+ * ensuring consistency and extensibility.
+ *
+ *
+ * If you intend to create custom mesh modifiers, you must implement this
+ * interface to ensure compatibility with the existing system and other mesh
+ * transformations.
+ *
+ *
+ * The {@code modify} method performs an in-place modification of the provided
+ * mesh and returns the same reference as the one passed in. This ensures that
+ * changes are directly applied to the provided instance, avoiding unnecessary
+ * object creation and improving performance.
+ *
+ */
public interface IMeshModifier {
- public Mesh3D modify(Mesh3D mesh);
+ /**
+ * Applies a modification to the provided 3D mesh.
+ *
+ *
+ * This method modifies the given mesh directly and returns the same reference
+ * that was provided. This is a design choice to avoid creating new objects
+ * unnecessarily and to enable efficient transformations on the provided mesh
+ * data.
+ *
+ *
+ * @param mesh The 3D mesh to modify. Must not be null.
+ * @return The same 3D mesh reference that was passed in, representing its
+ * modified state.
+ * @throws IllegalArgumentException if the provided mesh is null.
+ *
+ * @see mesh.modifier.* examples for concrete implementations
+ */
+ public Mesh3D modify(Mesh3D mesh);
}
diff --git a/src/main/java/mesh/modifier/InflateModifier.java b/src/main/java/mesh/modifier/InflateModifier.java
new file mode 100644
index 00000000..6d7140f9
--- /dev/null
+++ b/src/main/java/mesh/modifier/InflateModifier.java
@@ -0,0 +1,189 @@
+package mesh.modifier;
+
+import java.util.List;
+
+import math.Vector3f;
+import mesh.Mesh3D;
+import mesh.util.VertexNormals;
+
+/**
+ * A modifier that inflates or deflates a 3D mesh by displacing its vertices
+ * along their normals. This creates an effect of the mesh expanding outward or
+ * inward, similar to inflation or deflation.
+ *
+ * The inflation factor controls the degree of displacement, and the direction
+ * defines whether the mesh is inflated (expanded outward) or deflated
+ * (compressed inward).
+ */
+public class InflateModifier implements IMeshModifier {
+
+ /**
+ * The inflation factor determines how much the mesh will be inflated or
+ * deflated.
+ */
+ private float inflationFactor;
+
+ /**
+ * The inflation amount is calculated based on the inflation factor and
+ * direction. This value is used to displace each vertex along its normal.
+ */
+ private float inflationAmount;
+
+ /**
+ * The direction of inflation for the mesh. It can either be
+ * {@link Direction#OUTWARD} for outward inflation or {@link Direction#INWARD}
+ * for inward deflation.
+ */
+ private Direction direction;
+
+ /**
+ * The mesh that will be modified by this modifier.
+ */
+ private Mesh3D mesh;
+
+ /**
+ * A list of vertex normals for the mesh, used to determine the direction of
+ * inflation for each vertex. These normals are used to calculate the
+ * displacement of vertices.
+ */
+ private List vertexNormals;
+
+ /**
+ * The direction in which the mesh will inflate.
+ */
+ public enum Direction {
+ OUTWARD, INWARD
+ }
+
+ /**
+ * Creates a new InflateModifier with the specified inflation factor and
+ * direction.
+ *
+ * @param inflationFactor the factor by which the mesh will be inflated or
+ * deflated, must be positive
+ * @param direction the direction in which the inflation will occur
+ * @throws IllegalArgumentException if the inflationFactor is negative
+ * @throws IllegalArgumentException if the direction is null
+ */
+ public InflateModifier(float inflationFactor, Direction direction) {
+ setInflationFactor(inflationFactor);
+ setDirection(direction);
+ }
+
+ /**
+ * Modifies the given mesh by inflating or deflating its vertices. If the
+ * inflation factor is zero, no modification is performed.
+ *
+ * @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");
+ }
+ if (inflationFactor == 0) {
+ return mesh;
+ }
+ setMesh(mesh);
+ calculateInflationAmount();
+ calculateVertexNormals();
+ inflate();
+ return mesh;
+ }
+
+ /**
+ * Inflates or deflates the vertices of the mesh based on the inflation factor
+ * and direction.
+ */
+ private void inflate() {
+ for (int index = 0; index < mesh.getVertexCount(); index++) {
+ inflateVertexAt(index);
+ }
+ }
+
+ /**
+ * Inflates or deflates a specific vertex by applying the calculated inflation
+ * amount.
+ *
+ * @param index the index of the vertex to inflate
+ */
+ private void inflateVertexAt(int index) {
+ Vector3f vertex = mesh.getVertexAt(index);
+ Vector3f normal = vertexNormals.get(index);
+ vertex.addLocal(normal.mult(inflationAmount));
+ }
+
+ /**
+ * Calculates the vertex normals for the mesh to determine the direction of
+ * inflation for each vertex.
+ */
+ private void calculateVertexNormals() {
+ vertexNormals = new VertexNormals(mesh).getVertexNormals();
+ }
+
+ /**
+ * Calculates the inflation amount based on the inflation factor and direction.
+ */
+ private void calculateInflationAmount() {
+ inflationAmount = inflationFactor * (direction == Direction.OUTWARD ? 1 : -1);
+ }
+
+ /**
+ * Sets the mesh that will be modified.
+ *
+ * @param mesh the mesh to set
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Gets the inflation factor used to modify the mesh.
+ *
+ * @return the inflation factor
+ */
+ public float getInflationFactor() {
+ return inflationFactor;
+ }
+
+ /**
+ * Sets the inflation factor. A value of 0 means no inflation, while a positive
+ * value inflates the mesh. Negative values are not allowed.
+ *
+ * @param inflationFactor the inflation factor to set
+ * @throws IllegalArgumentException if the inflation factor is negative
+ */
+ public void setInflationFactor(float inflationFactor) {
+ if (inflationFactor < 0) {
+ throw new IllegalArgumentException("Inflation factor cannot be negative.");
+ }
+ this.inflationFactor = inflationFactor;
+ calculateInflationAmount();
+ }
+
+ /**
+ * Gets the direction of the inflation (outward or inward).
+ *
+ * @return the direction of inflation
+ */
+ public Direction getDirection() {
+ return direction;
+ }
+
+ /**
+ * Sets the direction of the inflation.
+ *
+ * @param direction the direction of inflation (OUTWARD or INWARD)
+ * @throws IllegalArgumentException if the direction is null
+ */
+ public void setDirection(Direction direction) {
+ if (direction == null) {
+ throw new IllegalArgumentException("Direction cannot be null.");
+ }
+ this.direction = direction;
+ calculateInflationAmount();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/mesh/modifier/InsetModifier.java b/src/main/java/mesh/modifier/InsetModifier.java
index ee6799c0..880740bd 100644
--- a/src/main/java/mesh/modifier/InsetModifier.java
+++ b/src/main/java/mesh/modifier/InsetModifier.java
@@ -4,74 +4,129 @@
import java.util.Collection;
import java.util.List;
+import math.GeometryUtil;
import math.Vector3f;
import mesh.Face3D;
import mesh.Mesh3D;
-public class InsetModifier implements IMeshModifier {
+public class InsetModifier implements IMeshModifier, FaceModifier {
+
+ private static final float DEFAULT_INSET = 0.1f;
+
+ private int nextIndex;
private float inset;
+ private Mesh3D mesh;
+
public InsetModifier() {
-
+ this(DEFAULT_INSET);
}
-
+
public InsetModifier(float inset) {
this.inset = inset;
}
@Override
public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
modify(mesh, mesh.getFaces());
return mesh;
}
- public void modify(Mesh3D mesh, Collection faces) {
- for (Face3D face : faces)
- modify(mesh, face);
+ @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.");
+ }
+ setMesh(mesh);
+ for (Face3D face : faces) {
+ processFace(face);
+ }
+ return mesh;
}
- public void modify(Mesh3D mesh, Face3D face) {
- int n = face.indices.length;
- int idx = mesh.vertices.size();
+ @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.");
+ }
+ setMesh(mesh);
+ processFace(face);
+ return mesh;
+ }
- List verts = new ArrayList();
+ private List processFaceEdges(Face3D face) {
+ List verts = 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];
- for (int i = 0; i < n; i++) {
- Vector3f v0 = mesh.vertices.get(face.indices[i]);
- Vector3f v1 = mesh.vertices.get(face.indices[(i + 1) % face.indices.length]);
+ Vector3f v0 = mesh.vertices.get(index0);
+ Vector3f v1 = mesh.vertices.get(index1);
- float distance = v0.distance(v1);
- float a = (1f / distance) * inset;
+ float edgeLength = v0.distance(v1);
+ float insetFactor = calculateInsetFactor(edgeLength);
- Vector3f v4 = v1.subtract(v0).mult(a).add(v0);
- Vector3f v5 = v1.add(v1.subtract(v0).mult(-a));
+ 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;
+ }
- for (int i = 1; i < verts.size(); i += 2) {
- int a = verts.size() - 2 + i;
- Vector3f v0 = verts.get(a % verts.size());
- Vector3f v1 = verts.get((a + 1) % verts.size());
- Vector3f v = v1.add(v0).mult(0.5f);
- mesh.add(v);
+ private void processFace(Face3D face) {
+ updateNextIndex();
+ createInsetVertices(processFaceEdges(face));
+ for (int i = 0; i < face.getVertexCount(); i++) {
+ createFaceAt(face, i);
}
-
- for (int i = 0; i < n; i++) {
- int index0 = face.indices[i];
- int index1 = face.indices[(i + 1) % n];
- int index2 = idx + ((i + 1) % n);
- int index3 = idx + i;
- mesh.addFace(index0, index1, index2, index3);
+ for (int i = 0; i < face.getVertexCount(); i++) {
+ face.indices[i] = nextIndex + i;
}
+ }
- for (int i = 0; i < n; i++) {
- face.indices[i] = idx + i;
+ 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 = GeometryUtil.getMidpoint(v0, v1);
+ mesh.add(v);
}
}
+ private void createFaceAt(Face3D face, int i) {
+ int n = face.indices.length;
+ int index0 = face.indices[i];
+ int index1 = face.indices[(i + 1) % n];
+ int index2 = nextIndex + ((i + 1) % n);
+ int index3 = nextIndex + i;
+ mesh.addFace(index0, index1, index2, index3);
+ }
+
+ private float calculateInsetFactor(float edgeLength) {
+ return edgeLength > 0 ? (1f / edgeLength) * inset : 0f;
+ }
+
+ private void updateNextIndex() {
+ nextIndex = mesh.vertices.size();
+ }
+
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
public float getInset() {
return inset;
}
diff --git a/src/main/java/mesh/modifier/PseudoWireframeModifier.java b/src/main/java/mesh/modifier/PseudoWireframeModifier.java
new file mode 100644
index 00000000..d2958e53
--- /dev/null
+++ b/src/main/java/mesh/modifier/PseudoWireframeModifier.java
@@ -0,0 +1,156 @@
+package mesh.modifier;
+
+import mesh.Mesh3D;
+
+/**
+ * Transforms a 3D mesh into a wireframe-like structure by creating holes and
+ * solidifying the remaining geometry. This is a pseudo-wireframe modifier,
+ * differing from traditional wireframe modifiers.
+ *
+ * Traditional wireframe modifiers, such as the one in Blender, replace the
+ * edges of the mesh with cylindrical or rectangular struts, directly converting
+ * the mesh's edges into a visible wireframe. In contrast, this pseudo-wireframe
+ * approach combines a hole-creation process with solidification to approximate
+ * the effect, maintaining the original surface structure with modifications.
+ *
+ */
+public class PseudoWireframeModifier implements IMeshModifier {
+
+ private static final float DEFAULT_HOLE_PECENTAGE = 0.9f;
+
+ private static final float DEFAULT_THICKNESS = 0.02f;
+
+ private float holePercentage;
+
+ private float thickness;
+
+ private Mesh3D mesh;
+
+ /**
+ * Constructs a new instance of the PseudoWireframeModifier with default
+ * parameters. The default hole percentage is set to 0.9, and the default
+ * solidify thickness is 0.02.
+ */
+ public PseudoWireframeModifier() {
+ holePercentage = DEFAULT_HOLE_PECENTAGE;
+ thickness = DEFAULT_THICKNESS;
+ }
+
+ /**
+ * Modifies the provided mesh to create a pseudo-wireframe-like effect by
+ * creating holes and applying solidification.
+ *
+ * @param mesh The 3D mesh to modify.
+ * @return The modified 3D mesh with pseudo-wireframe-like transformation
+ * applied.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ validateMesh(mesh);
+ setMesh(mesh);
+ createHoles();
+ solidify();
+ return mesh;
+ }
+
+ /**
+ * Creates holes in the mesh by applying a custom extrusion modifier configured
+ * to act as a "hole-creation" operation.
+ */
+ private void createHoles() {
+ mesh.apply(createExtrudeModifier());
+ }
+
+ /**
+ * Creates a configured instance of an ExtrudeModifier to perform the
+ * hole-creation effect.
+ *
+ * @return An instance of ExtrudeModifier configured with the current hole
+ * percentage.
+ */
+ private ExtrudeModifier createExtrudeModifier() {
+ ExtrudeModifier extrude = new ExtrudeModifier();
+ extrude.setScale(holePercentage);
+ extrude.setAmount(0);
+ extrude.setRemoveFaces(true);
+ return extrude;
+ }
+
+ /**
+ * Solidifies the remaining geometry after the hole-creation step by applying a
+ * solidification modifier with the defined thickness.
+ */
+ private void solidify() {
+ mesh.apply(new SolidifyModifier(thickness));
+ }
+
+ /**
+ * Validates that the provided mesh is not null before processing.
+ *
+ * @param mesh The mesh to validate.
+ * @throws IllegalArgumentException If the provided mesh is null.
+ */
+ private void validateMesh(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ }
+
+ /**
+ * Sets the current mesh instance for transformation operations.
+ *
+ * @param mesh The 3D mesh to set.
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Retrieves the current hole percentage value used for the pseudo-wireframe
+ * effect.
+ *
+ * @return The current hole percentage value.
+ */
+ public float getHolePercentage() {
+ return holePercentage;
+ }
+
+ /**
+ * Sets the percentage of the "holes" to apply on the mesh. Value must be
+ * between 0 and 1.
+ *
+ * @param holePercentage The new hole percentage value.
+ * @throws IllegalArgumentException If the value is less than 0 or greater than
+ * 1.
+ */
+ public void setHolePercentage(float holePercentage) {
+ if (holePercentage < 0 || holePercentage > 1) {
+ throw new IllegalArgumentException("Hole percentage must be between 0 and 1.");
+ }
+ this.holePercentage = holePercentage;
+ }
+
+ /**
+ * Retrieves the current thickness value used for solidification.
+ *
+ * @return The current solidify thickness value.
+ */
+ public float getThickness() {
+ return thickness;
+ }
+
+ /**
+ * Sets the solidify thickness value for the mesh transformation. Thickness must
+ * be positive.
+ *
+ * @param thickness The new thickness value.
+ * @throws IllegalArgumentException If the value is less than or equal to 0.
+ */
+ public void setThickness(float thickness) {
+ if (thickness <= 0) {
+ throw new IllegalArgumentException("Thickness must be greater than zero.");
+ }
+ this.thickness = thickness;
+ }
+
+}
diff --git a/src/main/java/mesh/modifier/PushPullModifier.java b/src/main/java/mesh/modifier/PushPullModifier.java
index d6a4ae11..61bde183 100644
--- a/src/main/java/mesh/modifier/PushPullModifier.java
+++ b/src/main/java/mesh/modifier/PushPullModifier.java
@@ -3,59 +3,140 @@
import math.Vector3f;
import mesh.Mesh3D;
+/**
+ * A modifier that adjusts the vertices of a 3D mesh by displacing them radially
+ * relative to a specified center point. The displacement is determined by the
+ * difference between the target radius and the current distance of each vertex
+ * from the center.
+ *
+ * This modifier can be used to create effects like expanding or contracting a
+ * mesh radially, forming shapes such as spheres, domes, or pits.
+ */
public class PushPullModifier implements IMeshModifier {
- private float distance;
+ private static final float EPSILON = 1e-6f;
+ /** Target radius for radial displacement. */
+ private float targetRadius;
+
+ /** Center point for radial displacement. */
private Vector3f center;
+ /** The mesh being modified. */
private Mesh3D mesh;
+ /**
+ * Default constructor. Initializes with zero displacement and origin (0, 0, 0)
+ * as center.
+ */
public PushPullModifier() {
- this(0, Vector3f.ZERO);
+ this(0, new Vector3f());
}
- public PushPullModifier(float distance, Vector3f center) {
- this.distance = distance;
- this.center = center;
+ /**
+ * Constructs a PushPullModifier with the specified radius and center point. The
+ * center cannot be null.
+ *
+ * @param targetRadius the target radius for vertex displacement
+ * @param center the center point for radial displacement
+ * @throws IllegalArgumentException if center is null
+ */
+ public PushPullModifier(float targetRadius, Vector3f center) {
+ setTargetRadius(targetRadius);
+ setCenter(center);
+ }
+
+ /**
+ * Modifies the mesh by displacing its vertices radially relative to the center
+ * point.
+ *
+ * @param mesh the mesh to modify
+ * @return the modified mesh
+ * @throws IllegalArgumentException if the provided mesh is null
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ pushPullVertices();
+ return mesh;
}
+ /**
+ * Displaces each vertex radially relative to the center point.
+ */
private void pushPullVertices() {
- for (Vector3f vertex : mesh.vertices)
- pushPullVertex(vertex);
+ mesh.vertices.parallelStream().forEach(this::pushPullVertex);
}
+ /**
+ * Displaces a single vertex radially based on its distance to the center.
+ *
+ * @param vertex the vertex to modify
+ */
private void pushPullVertex(Vector3f vertex) {
float distanceToCenter = vertex.distance(center);
- float displacement = distance - distanceToCenter;
+ if (Math.abs(distanceToCenter) < EPSILON) {
+ // Vertices exactly at the center will result in a NaN value during
+ // normalization in directionToCenter. This check skips such vertices.
+ return;
+ }
+ float displacement = targetRadius - distanceToCenter;
Vector3f directionToCenter = vertex.subtract(center).normalize();
vertex.set(directionToCenter.mult(displacement).add(center));
}
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- setMesh(mesh);
- pushPullVertices();
- return mesh;
- }
-
+ /**
+ * Sets the mesh to be modified.
+ *
+ * @param mesh the mesh to modify.
+ */
private void setMesh(Mesh3D mesh) {
this.mesh = mesh;
}
- public float getDistance() {
- return distance;
+ /**
+ * Gets the target radius for radial displacement.
+ *
+ * @return the target radius
+ */
+ public float getTargetRadius() {
+ return targetRadius;
}
- public void setDistance(float distance) {
- this.distance = distance;
+ /**
+ * Sets the target radius for radial displacement.
+ *
+ * @param targetRadius the target radius to set
+ */
+ public void setTargetRadius(float targetRadius) {
+ this.targetRadius = targetRadius;
}
+ /**
+ * Gets the center point for radial displacement.
+ *
+ * @return the center point
+ */
public Vector3f getCenter() {
return center;
}
+ /**
+ * Sets the center point for radial displacement. The center cannot be null.
+ *
+ * @param center the center point to set
+ * @throws IllegalArgumentException if the center is null
+ */
public void setCenter(Vector3f center) {
+ if (center == null) {
+ throw new IllegalArgumentException("Center cannot be null.");
+ }
this.center = center;
}
diff --git a/src/main/java/mesh/modifier/RippleModifier.java b/src/main/java/mesh/modifier/RippleModifier.java
new file mode 100644
index 00000000..8918bded
--- /dev/null
+++ b/src/main/java/mesh/modifier/RippleModifier.java
@@ -0,0 +1,392 @@
+package mesh.modifier;
+
+import math.Mathf;
+import math.Vector3f;
+import mesh.Mesh3D;
+
+/**
+ * The RippleModifier applies a ripple effect to a 3D mesh by displacing
+ * vertices along a specified direction based on sinusoidal wave functions. This
+ * effect is inspired by the ripple modifier found in Autodesk 3ds Max,
+ * simulating water-like wave patterns on a mesh surface.
+ *
+ *
+ * Key Parameters:
+ * - time: Controls the temporal evolution of the ripple.
+ * - amplitude1, amplitude2: Determine the intensity of the primary and secondary waves.
+ * - wavelength: Sets the distance between wave peaks.
+ * - phaseShift: Shifts the phase of the wave pattern.
+ * - decayFactor: Controls the attenuation of waves over time.
+ * - center: Defines the origin of the ripple.
+ *
+ *
+ * By adjusting these parameters, you can create various ripple effects, from
+ * gentle undulations to intense turbulence.
+ */
+public class RippleModifier implements IMeshModifier {
+
+ /**
+ * Represents the time progression of the ripple. Higher values simulate wave
+ * movement over time.
+ */
+ private float time;
+
+ /**
+ * Amplitude of the primary sine wave.
+ */
+ private float amplitude1;
+
+ /**
+ * Amplitude of the secondary cosine wave.
+ */
+ private float amplitude2;
+
+ /**
+ * Distance between successive wave peaks. Smaller values create denser ripples.
+ */
+ private float waveLength;
+
+ /**
+ * The wave number of the ripple, which determines the frequency of the
+ * sinusoidal wave used to calculate vertex displacement.
+ */
+ private float waveNumber;
+
+ /**
+ * Shifts the phase of the wave pattern, controlling where waves begin.
+ */
+ private float phaseShift;
+
+ /**
+ * Controls the attenuation of waves over time.
+ */
+ private float decayFactor;
+
+ /**
+ * Sets the origin of the ripple. Vertices closer to this point experience
+ * stronger displacement.
+ */
+ private Vector3f center;
+
+ /**
+ * Defines the direction in which the ripple effect displaces vertices in the
+ * mesh.
+ */
+ private Vector3f direction;
+
+ /**
+ * The mesh to which the ripple effect is applied.
+ */
+ private Mesh3D mesh;
+
+ /**
+ * Constructs a new {@link RippleModifier} with default values for the ripple
+ * effect parameters. The default values are:
+ *
+ *
+ * Amplitude1: 1.0f
+ * Amplitude2: 0.5f
+ * Wave Length: 5.0f
+ * Decay Factor: 0.1f
+ * Center: (0, 0, 0)
+ * Direction: (0, -1, 0)
+ *
+ *
+ * These defaults create a gentle ripple effect that propagates downward in the
+ * Y-axis direction.
+ */
+ public RippleModifier() {
+ amplitude1 = 1.0f;
+ amplitude2 = 0.5f;
+ waveLength = 5.0f;
+ decayFactor = 0.1f;
+ center = new Vector3f(0, 0, 0);
+ direction = new Vector3f(0, -1, 0);
+ }
+
+ /**
+ * Modifies the given mesh by applying the ripple effect. This method displaces
+ * vertices based on their distance from the center, the vertex normals, and the
+ * configured parameters such as amplitude, wave length, and decay factor.
+ *
+ * @param mesh the 3D mesh to modify (must not be null)
+ * @return the modified mesh
+ * @throws IllegalArgumentException if the mesh is null
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh must not be null.");
+ }
+ setMesh(mesh);
+ calculateWaveNumber();
+ applyRippleToVertices();
+ return mesh;
+ }
+
+ /**
+ * Applies the ripple effect formula to all vertices of the mesh.
+ */
+ private void applyRippleToVertices() {
+ mesh.vertices.parallelStream().forEach(this::applyRippleToVertex);
+ }
+
+ /**
+ * Applies the ripple effect to a specific vertex.
+ *
+ * @param vertex to modify
+ */
+ private void applyRippleToVertex(Vector3f vertex) {
+ float wavePhaseInput = calculateWavePhaseInput(vertex);
+ float wave1 = Mathf.sin(wavePhaseInput);
+ float wave2 = Mathf.cos(wavePhaseInput);
+ float displacement = amplitude1 * wave1 + amplitude2 * wave2;
+
+ vertex.addLocal(direction.normalize().mult(displacement));
+ }
+
+ /**
+ * Computes the wave phase input based on the vertex's distance from the center
+ * and the configured ripple parameters.
+ *
+ * @param vertex the vertex for which to calculate the wave phase input
+ * @return the wave phase input, incorporating distance, phase shift, and decay
+ * factor
+ */
+ private float calculateWavePhaseInput(Vector3f vertex) {
+ float distanceToCenter = vertex.distance(center);
+ float wavePhaseInput = waveNumber * distanceToCenter - phaseShift - decayFactor * time;
+ return wavePhaseInput;
+ }
+
+ /**
+ * Calculates the wave number based on the provided wavelength. The wave number
+ * is used to determine the frequency of the sinusoidal wave function that
+ * drives the ripple effect.
+ */
+ private void calculateWaveNumber() {
+ waveNumber = Mathf.TWO_PI / waveLength;
+ }
+
+ /**
+ * Sets the mesh to be modified by the ripple effect. This method assigns the
+ * provided mesh as the target for transformations.
+ *
+ * @param mesh the {@link Mesh3D} to apply the ripple effect to (must not be
+ * null).
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Retrieves the time parameter of the ripple effect. The time parameter
+ * influences the temporal evolution of the ripple, simulating wave movement
+ * over time.
+ *
+ * @return the current time value controlling the ripple's progression.
+ */
+ public float getTime() {
+ return time;
+ }
+
+ /**
+ * Sets the time progression of the ripple. This value controls the temporal
+ * evolution of the ripple effect.
+ *
+ * @param time the time value, typically non-negative
+ */
+ public void setTime(float time) {
+ this.time = time;
+ }
+
+ /**
+ * Retrieves the amplitude of the primary sine wave. The amplitude controls the
+ * intensity of the primary wave's displacement effect.
+ *
+ * @return the amplitude of the primary wave.
+ */
+ public float getAmplitude1() {
+ return amplitude1;
+ }
+
+ /**
+ * Sets the amplitude of the primary sine wave. The amplitude determines the
+ * magnitude of the primary wave's effect on the mesh.
+ *
+ * @param amplitude1 the amplitude of the primary wave (must be greater or equal
+ * to 0).
+ * @throws IllegalArgumentException if amplitude1 is less than 0
+ */
+ public void setAmplitude1(float amplitude1) {
+ if (amplitude1 < 0) {
+ throw new IllegalArgumentException("Aplitude1 must be greater or equal to 0.");
+ }
+ this.amplitude1 = amplitude1;
+ }
+
+ /**
+ * Retrieves the amplitude of the secondary cosine wave. The amplitude controls
+ * the intensity of the secondary wave's displacement effect.
+ *
+ * @return the amplitude of the secondary wave.
+ */
+ public float getAmplitude2() {
+ return amplitude2;
+ }
+
+ /**
+ * Sets the amplitude of the secondary cosine wave. The amplitude determines the
+ * magnitude of the secondary wave's effect on the mesh.
+ *
+ * @param amplitude2 the amplitude of the secondary wave (must be greater or
+ * equal to 0).
+ * @throws IllegalArgumentException if amplitude2 is less than 0
+ */
+ public void setAmplitude2(float amplitude2) {
+ if (amplitude2 < 0) {
+ throw new IllegalArgumentException("Aplitude2 must be greater or equal to 0.");
+ }
+ this.amplitude2 = amplitude2;
+ }
+
+ /**
+ * Retrieves the wavelength of the ripple effect. The wavelength determines the
+ * distance between successive wave peaks.
+ *
+ * @return the current wavelength of the ripple effect.
+ */
+ public float getWaveLength() {
+ return waveLength;
+ }
+
+ /**
+ * Sets the wave length, which is the distance between successive wave peaks.
+ *
+ * @param waveLength the wave length (must be greater than 0)
+ * @throws IllegalArgumentException if waveLength is less than or equal to 0
+ */
+ public void setWaveLength(float waveLength) {
+ if (waveLength <= 0) {
+ throw new IllegalArgumentException("Wave length must be greater than 0.");
+ }
+ this.waveLength = waveLength;
+ }
+
+ /**
+ * Retrieves the phase shift of the ripple effect. The phase shift determines
+ * where the wave pattern starts, effectively shifting the ripple along its
+ * progression.
+ *
+ * @return the current phase shift value.
+ */
+ public float getPhaseShift() {
+ return phaseShift;
+ }
+
+ /**
+ * Sets the phase shift of the ripple effect, wrapping it to the range [0, 2π].
+ * This ensures that the phase shift stays within a valid range to avoid
+ * unexpected behaviors. Adjusting this value changes the starting point of the
+ * wave pattern, allowing you to offset the ripple phase.
+ *
+ * @param phaseShift the new phase shift value.
+ */
+ public void setPhaseShift(float phaseShift) {
+ // Step 1: Wrap phaseShift to the range [-2π, 2π) using fmod
+ this.phaseShift = Mathf.fmod(phaseShift, Mathf.TWO_PI);
+
+ // Step 2: If the result is negative, add TWO_PI to bring it into [0, 2π)
+ if (this.phaseShift < 0) {
+ this.phaseShift += Mathf.TWO_PI;
+ }
+
+ // Step 3: Special case: if phaseShift is exactly TWO_PI (i.e., full cycle), set
+ // it to 0
+ if (Math.abs(this.phaseShift - Mathf.TWO_PI) < 0.001) {
+ this.phaseShift = 0.0f;
+ }
+ }
+
+ /**
+ * Retrieves the decay factor of the ripple effect. The decay factor controls
+ * how the amplitude of the ripple decreases as it propagates outward from the
+ * center.
+ *
+ * @return the current decay factor value.
+ */
+ public float getDecayFactor() {
+ return decayFactor;
+ }
+
+ /**
+ * Sets the decay factor of the ripple effect. Higher values result in faster
+ * attenuation of the ripple's amplitude over distance, while lower values allow
+ * the ripple to sustain longer.
+ *
+ * @param decayFactor the new decay factor value.
+ */
+ public void setDecayFactor(float decayFactor) {
+ this.decayFactor = decayFactor;
+ }
+
+ /**
+ * Retrieves the center of the ripple effect. The center is the origin point
+ * from which the ripple propagates.
+ *
+ * @return the current center as a {@link Vector3f}.
+ */
+ public Vector3f getCenter() {
+ return center;
+ }
+
+ /**
+ * Sets the center point of the ripple effect. Vertices closer to this point
+ * experience a stronger displacement due to the ripple.
+ *
+ * @param center the new center point (must not be null)
+ * @throws IllegalArgumentException if the provided center is null
+ */
+ public void setCenter(Vector3f center) {
+ if (center == null) {
+ throw new IllegalArgumentException("Center must not be null.");
+ }
+ this.center = center;
+ }
+
+ /**
+ * Retrieves the direction of the ripple displacement. The direction determines
+ * the axis along which vertices are displaced when the ripple effect is
+ * applied.
+ *
+ * @return the current direction as a {@link Vector3f}.
+ * @see #setDirection()
+ */
+ public Vector3f getDirection() {
+ return direction;
+ }
+
+ /**
+ * Sets the direction of the ripple displacement. The direction vector
+ * determines the axis along which the ripple effect displaces vertices in the
+ * 3D mesh. It defines the direction of wave propagation, and vertices are
+ * displaced in this direction based on the calculated ripple effect. The
+ * direction vector is normalized automatically when set to ensure consistent
+ * scaling of the displacement.
+ *
+ *
+ * By default, the direction is set to (0, -1, 0), which means the ripple
+ * displaces vertices along the negative Y-axis.
+ *
+ *
+ * @param direction the new direction vector (must not be null).
+ * @throws IllegalArgumentException if the provided direction is null.
+ */
+ public void setDirection(Vector3f direction) {
+ if (direction == null) {
+ throw new IllegalArgumentException("Direction must not be null.");
+ }
+ this.direction = direction.normalize();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/mesh/modifier/RotateXModifier.java b/src/main/java/mesh/modifier/RotateXModifier.java
index aef45698..7c272c9b 100644
--- a/src/main/java/mesh/modifier/RotateXModifier.java
+++ b/src/main/java/mesh/modifier/RotateXModifier.java
@@ -5,50 +5,143 @@
import math.Vector3f;
import mesh.Mesh3D;
+/**
+ * A mesh modifier that applies a rotation to a 3D mesh around the X-axis.
+ *
+ * This class implements {@link IMeshModifier} to apply a rotation
+ * transformation to a given mesh. The rotation is defined by an angle in
+ * radians. It modifies the vertices of the mesh in place using a computed
+ * rotation matrix.
+ */
public class RotateXModifier implements IMeshModifier {
- private float angle;
+ /**
+ * The current angle of rotation in radians. Defines how much the mesh should be
+ * rotated about the X-axis.
+ */
+ private float angle;
- private Mesh3D mesh;
+ /**
+ * The 3D mesh that will be transformed by this modifier. Represents the
+ * collection of vertices to apply the rotation on.
+ */
+ private Mesh3D mesh;
- private Matrix3f rotationMatrix;
+ /**
+ * The 3x3 rotation matrix used to compute the rotation transformation. This
+ * matrix is updated whenever the angle changes to ensure the transformation
+ * corresponds to the current rotation.
+ */
+ private Matrix3f rotationMatrix;
- public RotateXModifier() {
- this(0);
- }
+ /**
+ * Constructs a {@link RotateXModifier} with an initial angle of 0 radians.
+ */
+ public RotateXModifier() {
+ this(0);
+ }
- public RotateXModifier(float angle) {
- this.angle = angle;
- }
+ /**
+ * Constructs a {@link RotateXModifier} with a specified initial angle.
+ *
+ * @param angle the initial angle of rotation in radians
+ */
+ public RotateXModifier(float angle) {
+ initializeRotationMatrix();
+ setAngle(angle);
+ }
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- setMesh(mesh);
- createRotationMatrix();
- rotateMesh();
- return mesh;
- }
+ /**
+ * Modifies the provided mesh by applying a rotation transformation around the
+ * X-axis.
+ *
+ * If the provided mesh contains no vertices, the method safely returns the mesh
+ * without changes.
+ *
+ * @param mesh the 3D mesh to rotate (must not be null)
+ * @return the modified mesh after rotation
+ * @throws IllegalArgumentException if the provided mesh is null
+ * @see #getAngle()
+ * @see #setAngle(float)
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ rotateMesh();
+ return mesh;
+ }
- private void createRotationMatrix() {
- rotationMatrix = new Matrix3f(1, 0, 0, 0, Mathf.cos(angle),
- -Mathf.sin(angle), 0, Mathf.sin(angle), Mathf.cos(angle));
- }
+ /**
+ * Initializes the rotation matrix to its default state.
+ */
+ private void initializeRotationMatrix() {
+ rotationMatrix = new Matrix3f();
+ }
- private void rotateMesh() {
- for (Vector3f v : mesh.vertices)
- v.multLocal(rotationMatrix);
- }
+ /**
+ * Updates the rotation matrix based on the current angle of rotation. The
+ * matrix represents a 3D rotation transformation around the X-axis.
+ */
+ private void updateRotationMatrix() {
+ rotationMatrix.set(
+ 1, 0, 0,
+ 0, Mathf.cos(angle), -Mathf.sin(angle),
+ 0, Mathf.sin(angle), Mathf.cos(angle));
+ }
- private void setMesh(Mesh3D mesh) {
- this.mesh = mesh;
- }
+ /**
+ * Applies the rotation transformation to all vertices of the mesh using
+ * parallel execution.
+ */
+ private void rotateMesh() {
+ mesh.vertices.parallelStream().forEach(this::applyRotationToVertex);
+ }
- public float getAngle() {
- return angle;
- }
+ /**
+ * Applies the rotation matrix to a single vertex to transform it according to
+ * the current rotation.
+ *
+ * @param vertex the vertex to transform
+ */
+ private void applyRotationToVertex(Vector3f vertex) {
+ vertex.multLocal(rotationMatrix);
+ }
- public void setAngle(float angle) {
- this.angle = angle;
- }
+ /**
+ * Assigns the provided mesh to this modifier for processing.
+ *
+ * @param mesh the 3D mesh to process
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Gets the current angle of rotation in radians.
+ *
+ * @return the current angle of rotation
+ */
+ public float getAngle() {
+ return angle;
+ }
+
+ /**
+ * Sets the rotation angle in radians and updates the transformation matrix if
+ * the angle has changed.
+ *
+ * @param angle the new rotation angle in radians
+ */
+ public void setAngle(float angle) {
+ if (this.angle == angle)
+ return;
+ this.angle = angle;
+ updateRotationMatrix();
+ }
}
diff --git a/src/main/java/mesh/modifier/RotateYModifier.java b/src/main/java/mesh/modifier/RotateYModifier.java
index 3d0545a0..c95fe609 100644
--- a/src/main/java/mesh/modifier/RotateYModifier.java
+++ b/src/main/java/mesh/modifier/RotateYModifier.java
@@ -5,50 +5,143 @@
import math.Vector3f;
import mesh.Mesh3D;
+/**
+ * A mesh modifier that applies a rotation to a 3D mesh around the Y-axis.
+ *
+ * This class implements {@link IMeshModifier} to apply a rotation
+ * transformation to a given mesh. The rotation is defined by an angle in
+ * radians. It modifies the vertices of the mesh in place using a computed
+ * rotation matrix.
+ */
public class RotateYModifier implements IMeshModifier {
- private float angle;
+ /**
+ * The current angle of rotation in radians. Defines how much the mesh should be
+ * rotated about the Y-axis.
+ */
+ private float angle;
- private Mesh3D mesh;
+ /**
+ * The 3D mesh that will be transformed by this modifier. Represents the
+ * collection of vertices to apply the rotation on.
+ */
+ private Mesh3D mesh;
- private Matrix3f rotationMatrix;
+ /**
+ * The 3x3 rotation matrix used to compute the rotation transformation. This
+ * matrix is updated whenever the angle changes to ensure the transformation
+ * corresponds to the current rotation.
+ */
+ private Matrix3f rotationMatrix;
- public RotateYModifier() {
- this(0);
- }
+ /**
+ * Constructs a {@link RotateYModifier} with an initial angle of 0 radians.
+ */
+ public RotateYModifier() {
+ this(0);
+ }
- public RotateYModifier(float angle) {
- this.angle = angle;
- }
+ /**
+ * Constructs a {@link RotateXModifier} with a specified initial angle.
+ *
+ * @param angle the initial angle of rotation in radians
+ */
+ public RotateYModifier(float angle) {
+ initializeRotationMatrix();
+ setAngle(angle);
+ }
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- setMesh(mesh);
- createRotationMatrix();
- rotateMesh();
- return mesh;
- }
+ /**
+ * Modifies the provided mesh by applying a rotation transformation around the
+ * Y-axis.
+ *
+ * If the provided mesh contains no vertices, the method safely returns the mesh
+ * without changes.
+ *
+ * @param mesh the 3D mesh to rotate (must not be null)
+ * @return the modified mesh after rotation
+ * @throws IllegalArgumentException if the provided mesh is null
+ * @see #getAngle()
+ * @see #setAngle(float)
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ rotateMesh();
+ return mesh;
+ }
- private void createRotationMatrix() {
- rotationMatrix = new Matrix3f(Mathf.cos(angle), 0, Mathf.sin(angle), 0,
- 1, 0, -Mathf.sin(angle), 0, Mathf.cos(angle));
- }
+ /**
+ * Initializes the rotation matrix to its default state.
+ */
+ private void initializeRotationMatrix() {
+ rotationMatrix = new Matrix3f();
+ }
- private void rotateMesh() {
- for (Vector3f v : mesh.vertices)
- v.multLocal(rotationMatrix);
- }
+ /**
+ * Updates the rotation matrix based on the current angle of rotation. The
+ * matrix represents a 3D rotation transformation around the Y-axis.
+ */
+ private void updateRotationMatrix() {
+ rotationMatrix.set(
+ Mathf.cos(angle), 0, Mathf.sin(angle),
+ 0, 1, 0,
+ -Mathf.sin(angle), 0, Mathf.cos(angle));
+ }
- private void setMesh(Mesh3D mesh) {
- this.mesh = mesh;
- }
+ /**
+ * Applies the rotation transformation to all vertices of the mesh using
+ * parallel execution.
+ */
+ private void rotateMesh() {
+ mesh.vertices.parallelStream().forEach(this::applyRotationToVertex);
+ }
- public float getAngle() {
- return angle;
- }
+ /**
+ * Applies the rotation matrix to a single vertex to transform it according to
+ * the current rotation.
+ *
+ * @param vertex the vertex to transform
+ */
+ private void applyRotationToVertex(Vector3f vertex) {
+ vertex.multLocal(rotationMatrix);
+ }
- public void setAngle(float angle) {
- this.angle = angle;
- }
+ /**
+ * Assigns the provided mesh to this modifier for processing.
+ *
+ * @param mesh the 3D mesh to process
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Gets the current angle of rotation in radians.
+ *
+ * @return the current angle of rotation
+ */
+ public float getAngle() {
+ return angle;
+ }
+
+ /**
+ * Sets the rotation angle in radians and updates the transformation matrix if
+ * the angle has changed.
+ *
+ * @param angle the new rotation angle in radians
+ */
+ public void setAngle(float angle) {
+ if (this.angle == angle)
+ return;
+ this.angle = angle;
+ updateRotationMatrix();
+ }
}
diff --git a/src/main/java/mesh/modifier/RotateZModifier.java b/src/main/java/mesh/modifier/RotateZModifier.java
index 3e40221b..10638885 100644
--- a/src/main/java/mesh/modifier/RotateZModifier.java
+++ b/src/main/java/mesh/modifier/RotateZModifier.java
@@ -5,50 +5,143 @@
import math.Vector3f;
import mesh.Mesh3D;
+/**
+ * A mesh modifier that applies a rotation to a 3D mesh around the Z-axis.
+ *
+ * This class implements {@link IMeshModifier} to apply a rotation
+ * transformation to a given mesh. The rotation is defined by an angle in
+ * radians. It modifies the vertices of the mesh in place using a computed
+ * rotation matrix.
+ */
public class RotateZModifier implements IMeshModifier {
- private float angle;
+ /**
+ * The current angle of rotation in radians. Defines how much the mesh should be
+ * rotated about the Z-axis.
+ */
+ private float angle;
- private Mesh3D mesh;
+ /**
+ * The 3D mesh that will be transformed by this modifier. Represents the
+ * collection of vertices to apply the rotation on.
+ */
+ private Mesh3D mesh;
- private Matrix3f rotationMatrix;
+ /**
+ * The 3x3 rotation matrix used to compute the rotation transformation. This
+ * matrix is updated whenever the angle changes to ensure the transformation
+ * corresponds to the current rotation.
+ */
+ private Matrix3f rotationMatrix;
- public RotateZModifier() {
- this(0);
- }
+ /**
+ * Constructs a {@link RotateZModifier} with an initial angle of 0 radians.
+ */
+ public RotateZModifier() {
+ this(0);
+ }
- public RotateZModifier(float angle) {
- this.angle = angle;
- }
+ /**
+ * Constructs a {@link RotateZModifier} with a specified initial angle.
+ *
+ * @param angle the initial angle of rotation in radians
+ */
+ public RotateZModifier(float angle) {
+ initializeRotationMatrix();
+ setAngle(angle);
+ }
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- setMesh(mesh);
- createRotationMatrix();
- rotateMesh();
- return mesh;
- }
+ /**
+ * Modifies the provided mesh by applying a rotation transformation around the
+ * Z-axis.
+ *
+ * If the provided mesh contains no vertices, the method safely returns the mesh
+ * without changes.
+ *
+ * @param mesh the 3D mesh to rotate (must not be null)
+ * @return the modified mesh after rotation
+ * @throws IllegalArgumentException if the provided mesh is null
+ * @see #getAngle()
+ * @see #setAngle(float)
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ rotateMesh();
+ return mesh;
+ }
- private void createRotationMatrix() {
- rotationMatrix = new Matrix3f(Mathf.cos(angle), -Mathf.sin(angle), 0,
- Mathf.sin(angle), Mathf.cos(angle), 0, 0, 0, 1);
- }
+ /**
+ * Initializes the rotation matrix to its default state.
+ */
+ private void initializeRotationMatrix() {
+ rotationMatrix = new Matrix3f();
+ }
- private void rotateMesh() {
- for (Vector3f v : mesh.vertices)
- v.multLocal(rotationMatrix);
- }
+ /**
+ * Updates the rotation matrix based on the current angle of rotation. The
+ * matrix represents a 3D rotation transformation around the Z-axis.
+ */
+ private void updateRotationMatrix() {
+ rotationMatrix.set(
+ Mathf.cos(angle), -Mathf.sin(angle), 0,
+ Mathf.sin(angle), Mathf.cos(angle), 0,
+ 0, 0, 1);
+ }
- private void setMesh(Mesh3D mesh) {
- this.mesh = mesh;
- }
+ /**
+ * Applies the rotation transformation to all vertices of the mesh using
+ * parallel execution.
+ */
+ private void rotateMesh() {
+ mesh.vertices.parallelStream().forEach(this::applyRotationToVertex);
+ }
- public float getAngle() {
- return angle;
- }
+ /**
+ * Applies the rotation matrix to a single vertex to transform it according to
+ * the current rotation.
+ *
+ * @param vertex the vertex to transform
+ */
+ private void applyRotationToVertex(Vector3f vertex) {
+ vertex.multLocal(rotationMatrix);
+ }
- public void setAngle(float angle) {
- this.angle = angle;
- }
+ /**
+ * Assigns the provided mesh to this modifier for processing.
+ *
+ * @param mesh the 3D mesh to process
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Gets the current angle of rotation in radians.
+ *
+ * @return the current angle of rotation
+ */
+ public float getAngle() {
+ return angle;
+ }
+
+ /**
+ * Sets the rotation angle in radians and updates the transformation matrix if
+ * the angle has changed.
+ *
+ * @param angle the new rotation angle in radians
+ */
+ public void setAngle(float angle) {
+ if (this.angle == angle)
+ return;
+ this.angle = angle;
+ updateRotationMatrix();
+ }
}
diff --git a/src/main/java/mesh/modifier/ScaleModifier.java b/src/main/java/mesh/modifier/ScaleModifier.java
index a8a3bf3b..2dd1e34f 100644
--- a/src/main/java/mesh/modifier/ScaleModifier.java
+++ b/src/main/java/mesh/modifier/ScaleModifier.java
@@ -3,68 +3,157 @@
import math.Vector3f;
import mesh.Mesh3D;
+/**
+ * Scales a 3D mesh uniformly or non-uniformly along X, Y, and Z axes.
+ * Implements the {@link IMeshModifier} interface for modularity.
+ *
+ * This modifier scales all vertices of the provided 3D mesh based on the
+ * specified scaling factors (scaleX, scaleY, scaleZ).
+ */
public class ScaleModifier implements IMeshModifier {
- private float scaleX;
-
- private float scaleY;
-
- private float scaleZ;
-
- private Mesh3D mesh;
-
- public ScaleModifier() {
- this(1, 1, 1);
- }
-
- public ScaleModifier(float scale) {
- this(scale, scale, scale);
- }
-
- public ScaleModifier(float scaleX, float scaleY, float scaleZ) {
- this.scaleX = scaleX;
- this.scaleY = scaleY;
- this.scaleZ = scaleZ;
- }
-
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- setMesh(mesh);
- scaleMesh();
- return mesh;
- }
-
- private void scaleMesh() {
- for (Vector3f v : mesh.vertices)
- v.multLocal(scaleX, scaleY, scaleZ);
- }
-
- private void setMesh(Mesh3D mesh) {
- this.mesh = mesh;
- }
-
- public float getScaleX() {
- return scaleX;
- }
-
- public void setScaleX(float scaleX) {
- this.scaleX = scaleX;
- }
-
- public float getScaleY() {
- return scaleY;
- }
-
- public void setScaleY(float scaleY) {
- this.scaleY = scaleY;
- }
-
- public float getScaleZ() {
- return scaleZ;
- }
-
- public void setScaleZ(float scaleZ) {
- this.scaleZ = scaleZ;
- }
+ /** The scaling factor along the X-axis. */
+ private float scaleX;
+
+ /** The scaling factor along the Z-axis. */
+ private float scaleY;
+
+ /** The scaling factor along the Z-axis. */
+ private float scaleZ;
+
+ /** The 3D mesh currently being operated on by the modifier. */
+ private Mesh3D mesh;
+
+ /**
+ * Default constructor that initializes uniform scaling with factors (1, 1, 1).
+ */
+ public ScaleModifier() {
+ this(1, 1, 1);
+ }
+
+ /**
+ * Initializes the scaling factors uniformly along all axes.
+ *
+ * @param scale the uniform scale factor to apply across X, Y, and Z.
+ */
+ public ScaleModifier(float scale) {
+ this(scale, scale, scale);
+ }
+
+ /**
+ * Custom scaling constructor allowing different scaling factors for X, Y, and Z
+ * axes.
+ *
+ * @param scaleX the scaling factor along the X-axis.
+ * @param scaleY the scaling factor along the Y-axis.
+ * @param scaleZ the scaling factor along the Z-axis.
+ */
+ public ScaleModifier(float scaleX, float scaleY, float scaleZ) {
+ this.scaleX = scaleX;
+ this.scaleY = scaleY;
+ this.scaleZ = scaleZ;
+ }
+
+ /**
+ * Modifies the provided mesh by scaling all its vertices based on the scaling
+ * factors provided during construction or updates. If the provided mesh
+ * contains no vertices, the method safely returns the mesh without changes.
+ *
+ * @param mesh the 3D mesh to scale (must not be null).
+ * @return the scaled mesh.
+ * @throws IllegalArgumentException if the provided mesh is null.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ scaleMesh();
+ return mesh;
+ }
+
+ /**
+ * Scales all vertices of the associated mesh using parallel processing for
+ * improved performance.
+ */
+ private void scaleMesh() {
+ mesh.vertices.parallelStream().forEach(this::applyScaleToVertex);
+ }
+
+ /**
+ * Scales a single vertex by the given scaling factors.
+ *
+ * @param vertex the vertex to scale.
+ */
+ private void applyScaleToVertex(Vector3f vertex) {
+ vertex.multLocal(scaleX, scaleY, scaleZ);
+ }
+
+ /**
+ * Sets the mesh for this modifier to operate on.
+ *
+ * @param mesh the mesh to scale.
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Retrieves the scaling factor along the X-axis.
+ *
+ * @return the current scaling factor along the X-axis.
+ */
+ public float getScaleX() {
+ return scaleX;
+ }
+
+ /**
+ * Updates the scaling factor along the X-axis.
+ *
+ * @param scaleX the new scaling factor for the X-axis.
+ */
+ public void setScaleX(float scaleX) {
+ this.scaleX = scaleX;
+ }
+
+ /**
+ * Retrieves the scaling factor along the Y-axis.
+ *
+ * @return the current scaling factor along the Y-axis.
+ */
+ public float getScaleY() {
+ return scaleY;
+ }
+
+ /**
+ * Updates the scaling factor along the Y-axis.
+ *
+ * @param scaleY the new scaling factor for the Y-axis.
+ */
+ public void setScaleY(float scaleY) {
+ this.scaleY = scaleY;
+ }
+
+ /**
+ * Retrieves the scaling factor along the Z-axis.
+ *
+ * @return the current scaling factor along the Z-axis.
+ */
+ public float getScaleZ() {
+ return scaleZ;
+ }
+
+ /**
+ * Updates the scaling factor along the Z-axis.
+ *
+ * @param scaleZ the new scaling factor for the Z-axis.
+ */
+ public void setScaleZ(float scaleZ) {
+ this.scaleZ = scaleZ;
+ }
}
diff --git a/src/main/java/mesh/modifier/SnapToGroundModifier.java b/src/main/java/mesh/modifier/SnapToGroundModifier.java
index caa4f631..c635842d 100644
--- a/src/main/java/mesh/modifier/SnapToGroundModifier.java
+++ b/src/main/java/mesh/modifier/SnapToGroundModifier.java
@@ -3,12 +3,38 @@
import math.Vector3f;
import mesh.Mesh3D;
+/**
+ * The Snap to Ground Modifier is a utility designed to align a 3D mesh's
+ * highest point with a specified ground level. This modifier is particularly
+ * useful in scenarios such as terrain modeling, architectural visualization,
+ * and game development, where precise alignment with a reference plane is
+ * essential.
+ *
+ * The modifier calculates the vertical translation required to adjust the mesh,
+ * ensuring that its highest point rests exactly at the defined ground level.
+ *
+ *
+ * Key Considerations:
+ * - Simple Ground Plane: Assumes a flat, horizontal
+ * ground plane. It may not work as expected with complex terrain or curved
+ * surfaces.
+ * - Mesh Orientation: Assumes the Y-axis is the vertical axis.
+ * Meshes with different orientations might need preprocessing to achieve the
+ * desired results.
+ * - Mesh Topology: Works best with simple, closed meshes.
+ * Complex meshes with holes or self-intersections may require additional
+ * adjustments or processing.
+ *
+ */
public class SnapToGroundModifier implements IMeshModifier {
+ /** The level at which the mesh should be snapped. */
private float groundLevel;
+ /** The distance to translate the mesh to align it with the ground level. */
private float distanceToGround;
+ /** The mesh to be modified. */
private Mesh3D mesh;
public SnapToGroundModifier() {
@@ -19,28 +45,53 @@ public SnapToGroundModifier(float groundLevel) {
this.groundLevel = groundLevel;
}
+ /**
+ * Modifies the given mesh by snapping its highest point to the ground level. If
+ * the provided mesh contains no vertices, the method safely returns the mesh
+ * without changes.
+ *
+ * @param mesh the mesh to modify
+ * @return the modified mesh
+ * @throws IllegalArgumentException if the provided mesh is null
+ */
@Override
public Mesh3D modify(Mesh3D mesh) {
- if (mesh.vertices.isEmpty())
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (mesh.vertices.isEmpty()) {
return mesh;
-
+ }
setMesh(mesh);
calculateDistanceToGround();
translateMesh();
return mesh;
}
- private void translateMesh() {
- for (Vector3f vertex : mesh.getVertices()) {
- vertex.y += distanceToGround;
- }
- }
-
+ /**
+ * Calculates the vertical distance required to align the mesh's highest point
+ * with the ground level.
+ */
private void calculateDistanceToGround() {
float maxHeight = findHighestPoint();
distanceToGround = groundLevel - maxHeight;
}
+ /**
+ * Translates the entire mesh vertically to align its highest point with the
+ * ground level.
+ */
+ private void translateMesh() {
+ Vector3f delta = new Vector3f(0, distanceToGround, 0);
+ mesh.vertices.parallelStream().forEach(vertex -> vertex.addLocal(delta));
+ }
+
+ /**
+ * Finds the highest point (maximum Y-coordinate) among the vertices of the
+ * mesh.
+ *
+ * @return the Y-coordinate of the highest point
+ */
private float findHighestPoint() {
float max = mesh.getVertexAt(0).y;
for (Vector3f vertex : mesh.getVertices()) {
@@ -49,14 +100,29 @@ private float findHighestPoint() {
return max;
}
+ /**
+ * Sets the mesh to be modified.
+ *
+ * @param mesh the mesh to set
+ */
private void setMesh(Mesh3D mesh) {
this.mesh = mesh;
}
+ /**
+ * Gets the ground level that the mesh is snapped to.
+ *
+ * @return the ground level
+ */
public float getGroundLevel() {
return groundLevel;
}
+ /**
+ * Sets the ground level that the mesh should be snapped to.
+ *
+ * @param groundLevel the ground level to set
+ */
public void setGroundLevel(float groundLevel) {
this.groundLevel = groundLevel;
}
diff --git a/src/main/java/mesh/modifier/SpherifyModifier.java b/src/main/java/mesh/modifier/SpherifyModifier.java
index 76ff76a2..448d2677 100644
--- a/src/main/java/mesh/modifier/SpherifyModifier.java
+++ b/src/main/java/mesh/modifier/SpherifyModifier.java
@@ -3,57 +3,178 @@
import math.Vector3f;
import mesh.Mesh3D;
+/**
+ * The {@code SpherifyModifier} class modifies a 3D mesh by transforming its
+ * vertices to approximate the shape of a sphere. The degree of spherification
+ * is controlled by a factor between 0 and 1, with additional parameters for the
+ * sphere's radius and center.
+ */
public class SpherifyModifier implements IMeshModifier {
- private float factor;
+ /**
+ * The interpolation factor for spherification (0 = no effect, 1 = full sphere).
+ */
+ private float factor;
- private float radius;
+ /**
+ * The radius of the sphere used in the spherification process.
+ */
+ private float radius;
- private Vector3f center;
+ /**
+ * The center of the sphere.
+ */
+ private Vector3f center;
- public SpherifyModifier() {
- this(1f, new Vector3f());
- }
+ /**
+ * The mesh being modified.
+ */
+ private Mesh3D mesh;
- public SpherifyModifier(float radius) {
- this(radius, new Vector3f());
- }
+ /**
+ * Default constructor. Creates a spherify modifier with a default radius of 1.0
+ * and a factor of 1.0.
+ */
+ public SpherifyModifier() {
+ this(1.0f);
+ }
- public SpherifyModifier(float radius, Vector3f center) {
- this.radius = radius;
- this.center = center;
- this.factor = 1.0f;
- }
+ /**
+ * Constructor with a specified radius.
+ *
+ * @param radius the radius of the sphere. Must be greater than zero.
+ * @throws IllegalArgumentException if the radius is less than or equal to zero.
+ */
+ public SpherifyModifier(float radius) {
+ this.center = new Vector3f();
+ setRadius(radius);
+ setFactor(1.0f);
+ }
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- Vector3f origin = new Vector3f(center);
- for (Vector3f v0 : mesh.vertices) {
- Vector3f v1 = new Vector3f(v0.subtract(origin)).normalizeLocal()
- .mult(radius).add(origin);
- v0.lerpLocal(v1, factor);
- }
- return mesh;
- }
+ /**
+ * Modifies the given mesh by spherifying its vertices. If the provided mesh
+ * contains no vertices, the method safely returns the mesh without changes.
+ *
+ * @param mesh the mesh to modify. Cannot be {@code null}.
+ * @return the modified mesh.
+ * @throws IllegalArgumentException if the provided mesh is {@code null}.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (factor == 0) {
+ return mesh;
+ }
+ if (mesh.vertices.isEmpty()) {
+ return mesh;
+ }
+ setMesh(mesh);
+ spherify();
+ return mesh;
+ }
- public void setRadius(float radius) {
- this.radius = radius;
- }
+ /**
+ * Performs the spherification on the mesh vertices.
+ */
+ private void spherify() {
+ mesh.vertices.parallelStream().forEach(this::spherifyVertex);
+ }
- public void setCenter(float x, float y, float z) {
- center = new Vector3f(x, y, z);
- }
+ /**
+ * Spherifies a single vertex by interpolating its position toward the
+ * corresponding point on the sphere surface.
+ *
+ * @param vertex the vertex to modify.
+ */
+ private void spherifyVertex(Vector3f vertex) {
+ Vector3f direction = vertex.subtract(center).normalize();
+ Vector3f newPosition = direction.mult(radius).add(center);
+ vertex.lerpLocal(newPosition, factor);
+ }
- public void setCenter(Vector3f center) {
- this.center.set(center);
- }
+ /**
+ * Sets the mesh to be modified.
+ *
+ * @param mesh the mesh to modify.
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
- public float getFactor() {
- return factor;
- }
+ /**
+ * Returns the interpolation factor for spherification.
+ *
+ * @return the factor.
+ */
+ public float getFactor() {
+ return factor;
+ }
- public void setFactor(float factor) {
- this.factor = factor;
- }
+ /**
+ * Sets the interpolation factor for spherification.
+ *
+ * @param factor the factor, a value between 0 and 1.
+ * @throws IllegalArgumentException if the factor is not between 0 and 1.
+ */
+ public void setFactor(float factor) {
+ if (factor < 0 || factor > 1)
+ throw new IllegalArgumentException("Factor must be between 0 and 1.");
+ this.factor = factor;
+ }
+
+ /**
+ * Returns the radius of the sphere used in the spherification process.
+ *
+ * @return the radius.
+ */
+ public float getRadius() {
+ return radius;
+ }
+
+ /**
+ * Sets the radius of the sphere used in the spherification process.
+ *
+ * @param radius the radius. Must be greater than zero.
+ * @throws IllegalArgumentException if the radius is less than or equal to zero.
+ */
+ public void setRadius(float radius) {
+ if (radius <= 0)
+ throw new IllegalArgumentException("Radius must be greater than zero.");
+ this.radius = radius;
+ }
+
+ /**
+ * Returns the center of the sphere.
+ *
+ * @return the center of the sphere as a {@link Vector3f}.
+ */
+ public Vector3f getCenter() {
+ return new Vector3f(center);
+ }
+
+ /**
+ * Sets the center of the sphere using individual coordinates.
+ *
+ * @param x the x-coordinate of the center.
+ * @param y the y-coordinate of the center.
+ * @param z the z-coordinate of the center.
+ */
+ public void setCenter(float x, float y, float z) {
+ center.set(x, y, z);
+ }
+
+ /**
+ * Sets the center of the sphere using a {@link Vector3f}.
+ *
+ * @param center the center of the sphere. Cannot be {@code null}.
+ * @throws IllegalArgumentException if the center is {@code null}.
+ */
+ public void setCenter(Vector3f center) {
+ if (center == null)
+ throw new IllegalArgumentException("Center cannot be null.");
+ this.center.set(center);
+ }
}
diff --git a/src/main/java/mesh/modifier/TranslateModifier.java b/src/main/java/mesh/modifier/TranslateModifier.java
index 6d8ebadb..ba2d0984 100644
--- a/src/main/java/mesh/modifier/TranslateModifier.java
+++ b/src/main/java/mesh/modifier/TranslateModifier.java
@@ -3,62 +3,115 @@
import math.Vector3f;
import mesh.Mesh3D;
+/**
+ * TranslateModifier applies a translation (offset) to all vertices of a given
+ * 3D mesh.
+ *
+ * This modifier translates each vertex in the provided mesh by a given 3D
+ * vector (delta). The operation can be performed efficiently in parallel using
+ * Java's parallel streams for improved performance on large meshes.
+ */
public class TranslateModifier implements IMeshModifier {
- private float deltaX;
-
- private float deltaY;
-
- private float deltaZ;
+ /**
+ * The translation vector representing the offset in 3D space.
+ */
+ private Vector3f delta;
+ /**
+ * Default constructor initializes translation delta to (0, 0, 0).
+ */
public TranslateModifier() {
- this(0, 0, 0);
+ this.delta = new Vector3f(0, 0, 0);
}
- public TranslateModifier(Vector3f delta) {
- if (delta == null)
- throw new IllegalArgumentException();
-
- deltaX = delta.getX();
- deltaY = delta.getY();
- deltaZ = delta.getZ();
+ /**
+ * Constructs a TranslateModifier with specified translation offsets along each
+ * axis.
+ *
+ * @param deltaX Offset along the X-axis.
+ * @param deltaY Offset along the Y-axis.
+ * @param deltaZ Offset along the Z-axis.
+ */
+ public TranslateModifier(float deltaX, float deltaY, float deltaZ) {
+ this.delta = new Vector3f(deltaX, deltaY, deltaZ);
}
- public TranslateModifier(float deltaX, float deltaY, float deltaZ) {
- this.deltaX = deltaX;
- this.deltaY = deltaY;
- this.deltaZ = deltaZ;
+ /**
+ * Constructs a TranslateModifier using a Vector3f for the specified translation
+ * delta.
+ *
+ * @param delta The 3D translation vector to apply to the mesh's vertices.
+ * @throws IllegalArgumentException if the provided delta is null.
+ */
+ public TranslateModifier(Vector3f delta) {
+ if (delta == null) {
+ throw new IllegalArgumentException("Delta cannot be null.");
+ }
+ this.delta = new Vector3f(delta);
}
+ /**
+ * Applies the translation to all vertices in the provided mesh by adding the
+ * delta vector to each vertex. Uses parallel processing for efficiency.
+ *
+ * If the provided mesh contains no vertices, the method safely returns the mesh
+ * without changes.
+ *
+ * @param mesh The 3D mesh whose vertices will be translated.
+ * @return The modified 3D mesh after applying the translation.
+ * @throws IllegalArgumentException if the provided mesh is null.
+ */
@Override
public Mesh3D modify(Mesh3D mesh) {
- for (Vector3f v : mesh.vertices)
- v.addLocal(deltaX, deltaY, deltaZ);
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh cannot be null.");
+ }
+ if (!mesh.vertices.isEmpty()) {
+ mesh.vertices.parallelStream().forEach(vertex -> vertex.addLocal(delta));
+ }
return mesh;
}
- public float getDeltaX() {
- return deltaX;
+ /**
+ * Sets the translation of this modifiers delta to the values provided by the
+ * new delta.
+ *
+ * @param delta The new delta.
+ * @throws IllegalArgumentException if the provided delta is null.
+ */
+ public void setDelta(Vector3f delta) {
+ if (delta == null) {
+ throw new IllegalArgumentException("Delta cannot be null.");
+ }
+ this.delta.set(delta);
}
- public void setDeltaX(float deltaX) {
- this.deltaX = deltaX;
+ /**
+ * Retrieves the translation offset along the X-axis.
+ *
+ * @return The X component of the translation delta.
+ */
+ public float getDeltaX() {
+ return delta.x;
}
+ /**
+ * Retrieves the translation offset along the Y-axis.
+ *
+ * @return The Y component of the translation delta.
+ */
public float getDeltaY() {
- return deltaY;
- }
-
- public void setDeltaY(float deltaY) {
- this.deltaY = deltaY;
+ return delta.y;
}
+ /**
+ * Retrieves the translation offset along the Z-axis.
+ *
+ * @return The Z component of the translation delta.
+ */
public float getDeltaZ() {
- return deltaZ;
- }
-
- public void setDeltaZ(float deltaZ) {
- this.deltaZ = deltaZ;
+ return delta.z;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/mesh/modifier/UpdateFaceNormalsModifier.java b/src/main/java/mesh/modifier/UpdateFaceNormalsModifier.java
index 1fd0abe2..bf8d960c 100644
--- a/src/main/java/mesh/modifier/UpdateFaceNormalsModifier.java
+++ b/src/main/java/mesh/modifier/UpdateFaceNormalsModifier.java
@@ -1,18 +1,77 @@
package mesh.modifier;
+import math.Vector3f;
import mesh.Face3D;
import mesh.Mesh3D;
+/**
+ * The `UpdateFaceNormalsModifier` recalculates and updates the normals of all
+ * faces in a 3D mesh. This ensures that face normals are consistent with the
+ * current vertex positions and geometry.
+ *
+ *
+ * This modifier is useful when the geometry of the mesh has been modified, and
+ * accurate face normals are needed for rendering, physics calculations, or
+ * other operations.
+ *
+ */
public class UpdateFaceNormalsModifier implements IMeshModifier {
- @Override
- public Mesh3D modify(Mesh3D mesh) {
+ /** The mesh being modified. */
+ private Mesh3D mesh;
- for (Face3D face : mesh.faces) {
- face.normal = mesh.calculateFaceNormal(face);
- }
+ /**
+ * Recalculates and updates the normals of all faces in the given 3D mesh.
+ *
+ * @param mesh The 3D mesh to be modified.
+ * @return The modified 3D mesh with updated face normals.
+ * @throws IllegalArgumentException If the provided mesh is null.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ validateMesh(mesh);
+ setMesh(mesh);
+ updateFaceNormals();
+ return mesh;
+ }
- return mesh;
- }
+ /**
+ * Iterates over all faces in the mesh and updates their normals in parallel.
+ * Parallel processing is used to enhance performance for large meshes.
+ */
+ private void updateFaceNormals() {
+ mesh.faces.parallelStream().forEach(this::updateFaceNormal);
+ }
+
+ /**
+ * Calculates and updates the normal for a single face.
+ *
+ * @param face The face whose normal is to be updated.
+ */
+ private void updateFaceNormal(Face3D face) {
+ Vector3f normal = mesh.calculateFaceNormal(face);
+ face.normal.set(normal);
+ }
+
+ /**
+ * Validates that the provided 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.");
+ }
+ }
+
+ /**
+ * Sets the current mesh for processing.
+ *
+ * @param mesh The 3D mesh to set.
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
}
diff --git a/src/main/java/mesh/modifier/WaveModifier.java b/src/main/java/mesh/modifier/WaveModifier.java
new file mode 100644
index 00000000..d65d0cb9
--- /dev/null
+++ b/src/main/java/mesh/modifier/WaveModifier.java
@@ -0,0 +1,235 @@
+package mesh.modifier;
+
+import math.Mathf;
+import math.Vector3f;
+import mesh.Mesh3D;
+
+/**
+ * The {@code WaveModifier} class applies a wave-like deformation to a 3D mesh,
+ * simulating the appearance of sinusoidal waves across the surface of the
+ * mesh.The wave modifier is particularly useful for simulating effects like
+ * water surfaces, undulating terrain, or other wave-like phenomena in computer
+ * graphics. This modifier operates by displacing the vertices of a given mesh
+ * based on a mathematical wave function:
+ *
+ *
+ * y = A ⋅ sin((2π / λ) ⋅ (D ⋅ P) + ϕ)
+ *
+ * - A (Amplitude): Controls the height of the wave peaks.
+ * - λ (Wavelength): Determines the distance between consecutive wave peaks.
+ * - D (Direction): A normalized vector representing the direction of
+ * wave propagation.
+ * - P (Vertex Position): The position of each vertex in 3D space.
+ * - ϕ (Phase): An optional offset used to animate or shift the wave
+ * over time.
+ *
+ * Usage:
+ *
+ * Mesh3D mesh = new Mesh3D();
+ * WaveModifier modifier = new WaveModifier();
+ * modifier.setAmplitude(2.0f);
+ * modifier.setWavelength(5.0f);
+ * modifier.setPhase(1.0f);
+ * modifier.setDirection(new Vector3f(1, 0, 0));
+ * modifier.modify(mesh);
+ *
+ *
+ * @see Mesh3D
+ * @see IMeshModifier
+ */
+public class WaveModifier implements IMeshModifier {
+
+ /**
+ * Height of the wave peaks.
+ */
+ private float amplitude;
+
+ /**
+ * Distance between wave peaks.
+ */
+ private float wavelength;
+
+ /**
+ * Phase offset for animation.
+ */
+ private float phase;
+
+ /**
+ * Direction of the wave.
+ */
+ private Vector3f direction;
+
+ /**
+ * The mesh to be modified.
+ */
+ private Mesh3D mesh;
+
+ /**
+ * Constructs a new {@link WaveModifier} with default values for the wave effect
+ * parameters. The default values are:
+ *
+ *
+ * Amplitude: 1
+ * Wavelength: 1
+ * Phase: 0
+ * Direction: (1, 0, 0)
+ *
+ *
+ * These default settings generate a basic wave along the X-axis. Users can
+ * modify these properties using the corresponding setter methods after
+ * instantiation.
+ */
+ public WaveModifier() {
+ setAmplitude(1);
+ setWavelength(1);
+ setDirection(new Vector3f(1, 0, 0));
+ }
+
+ /**
+ * Applies the wave effect to the provided mesh by modifying its vertices by
+ * using the wave function defined in the {@link WaveModifier}.
+ *
+ * @param mesh The {@link Mesh3D} instance to be modified. This mesh must not be
+ * null.
+ * @return The modified {@link Mesh3D} with the wave effect applied to its
+ * vertices.
+ * @throws IllegalArgumentException If the provided mesh is null.
+ */
+ @Override
+ public Mesh3D modify(Mesh3D mesh) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh must not be null.");
+ }
+ setMesh(mesh);
+ applyWaveToVertices();
+ return mesh;
+ }
+
+ /**
+ * Applies the wave deformation to all vertices of the mesh. This method does
+ * not return a value; it directly modifies the vertex positions of the mesh in
+ * place.
+ */
+ private void applyWaveToVertices() {
+ mesh.vertices.parallelStream().forEach(this::applyWaveToVertex);
+ }
+
+ /**
+ * Applies a wave deformation to a single vertex based on a sinusoidal wave
+ * equation.
+ *
+ * @param vertex The 3D vertex to which the wave deformation is applied. The
+ * vertex's position is directly modified in place.
+ * @throws NullPointerException if the vertex is null.
+ */
+ private void applyWaveToVertex(Vector3f vertex) {
+ // Project the vertex position onto the wave direction
+ float projection = vertex.dot(direction);
+
+ // Calculate the wave offset using the wave function
+ float waveOffset = amplitude * Mathf.sin((2 * Mathf.PI / wavelength) * projection + phase);
+
+ // Apply the offset to the y-coordinate (height)
+ vertex.y += waveOffset;
+ }
+
+ /**
+ * Sets the mesh that will be modified by the wave effect.
+ *
+ * @param mesh The {@link Mesh3D} instance to modify. Must not be null.
+ */
+ private void setMesh(Mesh3D mesh) {
+ this.mesh = mesh;
+ }
+
+ /**
+ * Retrieves the amplitude of the wave.
+ *
+ * @return The amplitude, representing the height of the wave peaks.
+ */
+ public float getAmplitude() {
+ return amplitude;
+ }
+
+ /**
+ * Sets the amplitude of the wave.
+ *
+ * @param amplitude The height of the wave peaks. Must be non-negative.
+ * @throws IllegalArgumentException if the amplitude is negative.
+ */
+ public void setAmplitude(float amplitude) {
+ if (amplitude < 0) {
+ throw new IllegalArgumentException("Amplitude must be non-negative.");
+ }
+ this.amplitude = amplitude;
+ }
+
+ /**
+ * Retrieves the wavelength of the wave.
+ *
+ * @return The wavelength, representing the distance between consecutive wave
+ * peaks.
+ */
+ public float getWavelength() {
+ return wavelength;
+ }
+
+ /**
+ * Sets the wavelength of the wave.
+ *
+ * @param wavelength The distance between consecutive wave peaks. Must be
+ * positive.
+ * @throws IllegalArgumentException if the wavelength is less than or equal to
+ * zero.
+ */
+ public void setWavelength(float wavelength) {
+ if (wavelength <= 0) {
+ throw new IllegalArgumentException("Wavelength must be positive.");
+ }
+ this.wavelength = wavelength;
+ }
+
+ /**
+ * Retrieves the current phase offset of the wave.
+ *
+ * @return The phase offset, used to animate or shift the wave.
+ */
+ public float getPhase() {
+ return phase;
+ }
+
+ /**
+ * Sets the phase offset for the wave.
+ *
+ * @param phase The phase offset, typically used for animation.
+ */
+ public void setPhase(float phase) {
+ this.phase = phase;
+ }
+
+ /**
+ * Retrieves the direction of the wave.
+ *
+ * @return A {@link Vector3f} representing the unit vector of the wave's
+ * direction.
+ */
+ public Vector3f getDirection() {
+ return direction;
+ }
+
+ /**
+ * Sets the direction of the wave. The direction vector is normalized
+ * automatically.
+ *
+ * @param direction A {@link Vector3f} representing the direction of wave
+ * propagation. Must not be null or a zero vector.
+ * @throws IllegalArgumentException if the direction is null or a zero vector.
+ */
+ public void setDirection(Vector3f direction) {
+ if (direction == null || direction.equals(Vector3f.ZERO)) {
+ throw new IllegalArgumentException("Direction must not be null or zero.");
+ }
+ this.direction = direction.normalize();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/mesh/modifier/WidthType.java b/src/main/java/mesh/modifier/WidthType.java
deleted file mode 100644
index 5c7a9c2c..00000000
--- a/src/main/java/mesh/modifier/WidthType.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package mesh.modifier;
-
-public enum WidthType {
-
- OFFSET,
-
- WIDTH,
-
- DEPTH,
-
-}
\ No newline at end of file
diff --git a/src/main/java/mesh/modifier/WireframeModifier.java b/src/main/java/mesh/modifier/WireframeModifier.java
deleted file mode 100644
index 6286bf2f..00000000
--- a/src/main/java/mesh/modifier/WireframeModifier.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package mesh.modifier;
-
-import java.util.List;
-
-import mesh.Face3D;
-import mesh.Mesh3D;
-
-public class WireframeModifier implements IMeshModifier {
-
- private float scaleExtrude;
-
- private float thickness;
-
- private Mesh3D mesh;
-
- public WireframeModifier() {
- scaleExtrude = 0.9f;
- thickness = 0.02f;
- }
-
- private void createHoles() {
- List faces = mesh.getFaces();
- mesh.apply(new ExtrudeModifier(scaleExtrude, 0));
- mesh.faces.removeAll(faces);
- }
-
- private void solidify() {
- mesh.apply(new SolidifyModifier(thickness));
- }
-
- private void setMesh(Mesh3D mesh) {
- this.mesh = mesh;
- }
-
- @Override
- public Mesh3D modify(Mesh3D mesh) {
- setMesh(mesh);
- createHoles();
- solidify();
- return mesh;
- }
-
-}
diff --git a/src/main/java/mesh/util/Mesh3DUtil.java b/src/main/java/mesh/util/Mesh3DUtil.java
index 49035283..065a194a 100644
--- a/src/main/java/mesh/util/Mesh3DUtil.java
+++ b/src/main/java/mesh/util/Mesh3DUtil.java
@@ -5,109 +5,80 @@
import math.Vector3f;
import mesh.Face3D;
import mesh.Mesh3D;
+import mesh.modifier.ExtrudeModifier;
@Deprecated
public class Mesh3DUtil {
@Deprecated
- public static float perimeter(Mesh3D mesh, Face3D face) {
- float perimeter = 0;
- for (int i = 0; i < face.indices.length - 2; i++) {
- i++;
- Vector3f v0 = mesh.getVertexAt(face.indices[i]);
- Vector3f v1 = mesh.getVertexAt(face.indices[i + 1]);
- perimeter += v0.distance(v1);
- }
- return perimeter;
- }
+ public static float perimeter(Mesh3D mesh, Face3D face) {
+ float perimeter = 0;
+ for (int i = 0; i < face.indices.length - 2; i++) {
+ i++;
+ Vector3f v0 = mesh.getVertexAt(face.indices[i]);
+ Vector3f v1 = mesh.getVertexAt(face.indices[i + 1]);
+ perimeter += v0.distance(v1);
+ }
+ return perimeter;
+ }
@Deprecated
- private static void scaleFace(Mesh3D mesh, Face3D face, float scale) {
- Vector3f center = mesh.calculateFaceCenter(face);
- for (int i = 0; i < face.indices.length; i++) {
- Vector3f v = mesh.vertices.get(face.indices[i]);
- v.subtractLocal(center).multLocal(scale).addLocal(center);
- }
- }
+ private static void scaleFace(Mesh3D mesh, Face3D face, float scale) {
+ Vector3f center = mesh.calculateFaceCenter(face);
+ for (int i = 0; i < face.indices.length; i++) {
+ Vector3f v = mesh.vertices.get(face.indices[i]);
+ v.subtractLocal(center).multLocal(scale).addLocal(center);
+ }
+ }
@Deprecated
- public static void scaleFaceAt(Mesh3D mesh, int index, float scale) {
- Face3D f = mesh.faces.get(index);
- scaleFace(mesh, f, scale);
- }
+ public static void scaleFaceAt(Mesh3D mesh, int index, float scale) {
+ Face3D f = mesh.faces.get(index);
+ scaleFace(mesh, f, scale);
+ }
@Deprecated
- public static void rotateFaceX(Mesh3D mesh, Face3D face, float a) {
- Matrix3f m = new Matrix3f(
- 1, 0, 0, 0, Mathf.cos(a), -Mathf.sin(a), 0, Mathf.sin(a),
- Mathf.cos(a)
- );
+ public static void rotateFaceX(Mesh3D mesh, Face3D face, float a) {
+ Matrix3f m = new Matrix3f(1, 0, 0, 0, Mathf.cos(a), -Mathf.sin(a), 0, Mathf.sin(a), Mathf.cos(a));
- for (int i = 0; i < face.indices.length; i++) {
- Vector3f v = mesh.vertices.get(face.indices[i]);
- Vector3f v0 = v.mult(m);
- v.set(v.getX(), v0.getY(), v0.getZ());
- }
- }
+ for (int i = 0; i < face.indices.length; i++) {
+ Vector3f v = mesh.vertices.get(face.indices[i]);
+ Vector3f v0 = v.mult(m);
+ v.set(v.getX(), v0.getY(), v0.getZ());
+ }
+ }
@Deprecated
- public static void rotateFaceY(Mesh3D mesh, Face3D face, float a) {
- Matrix3f m = new Matrix3f(
- Mathf.cos(a), 0, Mathf.sin(a), 0, 1, 0, -Mathf.sin(a), 0,
- Mathf.cos(a)
- );
+ public static void rotateFaceY(Mesh3D mesh, Face3D face, float a) {
+ Matrix3f m = new Matrix3f(Mathf.cos(a), 0, Mathf.sin(a), 0, 1, 0, -Mathf.sin(a), 0, Mathf.cos(a));
- for (int i = 0; i < face.indices.length; i++) {
- Vector3f v = mesh.vertices.get(face.indices[i]);
- Vector3f v0 = v.mult(m);
- v.set(v0.getX(), v.getY(), v0.getZ());
- }
- }
+ for (int i = 0; i < face.indices.length; i++) {
+ Vector3f v = mesh.vertices.get(face.indices[i]);
+ Vector3f v0 = v.mult(m);
+ v.set(v0.getX(), v.getY(), v0.getZ());
+ }
+ }
@Deprecated
- public static void rotateFaceZ(Mesh3D mesh, Face3D face, float a) {
- Matrix3f m = new Matrix3f(
- Mathf.cos(a), -Mathf.sin(a), 0, Mathf.sin(a), Mathf.cos(a), 0,
- 0, 0, 1
- );
-
- for (int i = 0; i < face.indices.length; i++) {
- Vector3f v = mesh.vertices.get(face.indices[i]);
- Vector3f v0 = v.mult(m);
- v.set(v0.getX(), v0.getY(), v.getZ());
- }
- }
-
+ public static void rotateFaceZ(Mesh3D mesh, Face3D face, float a) {
+ Matrix3f m = new Matrix3f(Mathf.cos(a), -Mathf.sin(a), 0, Mathf.sin(a), Mathf.cos(a), 0, 0, 0, 1);
+
+ for (int i = 0; i < face.indices.length; i++) {
+ Vector3f v = mesh.vertices.get(face.indices[i]);
+ Vector3f v0 = v.mult(m);
+ v.set(v0.getX(), v0.getY(), v.getZ());
+ }
+ }
+
+ /**
+ * @deprecated Use {@link ExtrudeModifier} instead.
+ */
@Deprecated
- public static void extrudeFace(Mesh3D mesh, Face3D face, float scale,
- float amount) {
- int n = face.indices.length;
- int idx = mesh.vertices.size();
- Vector3f normal = mesh.calculateFaceNormal(face);
- Vector3f center = mesh.calculateFaceCenter(face);
-
- normal.multLocal(amount);
-
- for (int i = 0; i < n; i++) {
- Vector3f v0 = mesh.vertices.get(face.indices[i]);
- Vector3f v1 = new Vector3f(v0).subtract(center).mult(scale)
- .add(center);
-
- v1.addLocal(normal);
- mesh.vertices.add(v1);
- }
-
- for (int i = 0; i < n; i++) {
- Face3D f0 = new Face3D(
- face.indices[i], face.indices[(i + 1) % n],
- idx + ((i + 1) % n), idx + i
- );
- mesh.add(f0);
- }
-
- for (int i = 0; i < n; i++) {
- face.indices[i] = idx + i;
- }
- }
+ public static void extrudeFace(Mesh3D mesh, Face3D face, float scale, float amount) {
+ ExtrudeModifier modifier = new ExtrudeModifier();
+ modifier.setScale(scale);
+ modifier.setAmount(amount);
+ modifier.modify(mesh, face);
+ }
}
diff --git a/src/test/java/mesh/modifier/test/RippleModifierTest.java b/src/test/java/mesh/modifier/test/RippleModifierTest.java
new file mode 100644
index 00000000..8d63c030
--- /dev/null
+++ b/src/test/java/mesh/modifier/test/RippleModifierTest.java
@@ -0,0 +1,242 @@
+package mesh.modifier.test;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import math.Mathf;
+import math.Vector3f;
+import mesh.Mesh3D;
+import mesh.creator.primitives.CubeCreator;
+import mesh.modifier.IMeshModifier;
+import mesh.modifier.RippleModifier;
+
+public class RippleModifierTest {
+
+ private RippleModifier modifier;
+
+ @BeforeEach
+ public void setUp() {
+ modifier = new RippleModifier();
+ }
+
+ @Test
+ public void testModifierImplementsModifierInterface() {
+ assertTrue(modifier instanceof IMeshModifier);
+ }
+
+ @Test
+ public void testModifiedMeshIsNotNull() {
+ Mesh3D mesh = new Mesh3D();
+ assertNotNull(modifier.modify(mesh));
+ }
+
+ @Test
+ public void testReturnsReferenceToTheModifiedMesh() {
+ Mesh3D mesh0 = new CubeCreator().create();
+ Mesh3D mesh1 = modifier.modify(mesh0);
+ assertSame(mesh0, mesh1);
+ }
+
+ @Test
+ public void testDefaultValues() {
+ assertAll("Default Values",
+ () -> assertEquals(0, modifier.getTime()),
+ () -> assertEquals(1.0f, modifier.getAmplitude1()),
+ () -> assertEquals(0.5f, modifier.getAmplitude2()),
+ () -> assertEquals(5.0f, modifier.getWaveLength()),
+ () -> assertEquals(0.1f, modifier.getDecayFactor())
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { 1.0f, 15.002f, 20.245f })
+ public void testGetSetAmplitude1(float amplitude1) {
+ modifier.setAmplitude1(amplitude1);
+ assertEquals(amplitude1, modifier.getAmplitude1());
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { 1.12f, 11.0352f, 56.245f, 120.23f })
+ public void testGetSetAmplitude2(float amplitude2) {
+ modifier.setAmplitude2(amplitude2);
+ assertEquals(amplitude2, modifier.getAmplitude2());
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { 10.0f, 51.44f, 60.245f })
+ public void testGetSetWaveLength(float waveLength) {
+ modifier.setWaveLength(waveLength);
+ assertEquals(waveLength, modifier.getWaveLength());
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { 155.0f, 20.44f, -100.245f })
+ public void testGetSetDecayFactor(float decayFactor) {
+ modifier.setDecayFactor(decayFactor);
+ assertEquals(decayFactor, modifier.getDecayFactor());
+ }
+
+ @Test
+ public void testCenterIsNotNullByDefault() {
+ assertNotNull(modifier.getCenter());
+ }
+
+ @Test
+ public void testCenterIsAtOriginByDefault() {
+ assertEquals(Vector3f.ZERO, modifier.getCenter());
+ }
+
+ @Test
+ public void testGetSetCenter() {
+ Vector3f[] centers = new Vector3f[] {
+ new Vector3f(-0.134f, 1, 10.4f),
+ new Vector3f(102.34f, 332.431f, -0.4f),
+ new Vector3f(46.34f, -32.432f, 0.134f),
+ new Vector3f(0.001f, 0.32f, -2.34f)
+ };
+ for (int i = 0; i < centers.length; i++) {
+ modifier.setCenter(centers[i]);
+ assertEquals(centers[i], modifier.getCenter());
+ }
+ }
+
+ @Test
+ public void testSetCenterToNullThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> modifier.setCenter(null));
+ }
+
+ @Test
+ public void testSetWaveLengthToZeroThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> modifier.setWaveLength(0));
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { -1.0f, -15.002f, -20.245f, -100.23f })
+ public void testSetWaveLengthLessThanZeroThrowsException(float waveLength) {
+ assertThrows(IllegalArgumentException.class, () -> modifier.setWaveLength(waveLength));
+ }
+
+ @Test
+ public void testModifyNullMeshThrowsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> modifier.modify(null));
+ }
+
+ @Test
+ public void testNegativeAmplitude1ThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> modifier.setAmplitude1(-1));
+ }
+
+ @Test
+ public void testNegativeAmplitude2ThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> modifier.setAmplitude2(-1));
+ }
+
+ @Test
+ public void testDefaultDirection() {
+ Vector3f expected = new Vector3f(0, -1, 0);
+ assertEquals(expected, modifier.getDirection());
+ }
+
+ @Test
+ public void testSetNullDirectionThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> modifier.setDirection(null));
+ }
+
+ @Test
+ public void testDirectionIsNormalizedByDefault() {
+ float length = modifier.getDirection().length();
+ assertEquals(1, length);
+ }
+
+ @Test
+ public void testSetDirectionNormalizesDirection() {
+ Vector3f direction = new Vector3f(5, 43.45f, 1);
+ modifier.setDirection(direction);
+ float length = modifier.getDirection().length();
+ assertEquals(1, length, 0.0001f);
+ }
+
+ @Test
+ public void testDirectionIsNormalizedInternally() {
+ Vector3f expected = new Vector3f(1, 3.556f, 2.345f);
+ Vector3f direction = new Vector3f(expected);
+ modifier.setDirection(direction);
+ assertEquals(expected, direction);
+ }
+
+ @Test
+ public void testDefaultPhaseShift() {
+ assertEquals(0, modifier.getPhaseShift());
+ }
+
+ @Test
+ public void testPhaseShiftMultiplesOfTwoPi() {
+ float expected = Mathf.TWO_PI;
+ modifier.setPhaseShift(expected * 4);
+ assertEquals(0, modifier.getPhaseShift());
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { 0, Mathf.HALF_PI, Mathf.QUARTER_PI })
+ public void testGetSetPhaseShift(float phaseShift) {
+ modifier.setPhaseShift(phaseShift);
+ assertEquals(phaseShift, modifier.getPhaseShift());
+ }
+
+ @Test
+ public void testSetPhaseShiftPositive() {
+ RippleModifier modifier = new RippleModifier();
+ modifier.setPhaseShift(1.5f);
+ assertEquals(1.5f, modifier.getPhaseShift(), 0.001);
+ }
+
+ @Test
+ public void testSetPhaseShiftNegative() {
+ RippleModifier modifier = new RippleModifier();
+ modifier.setPhaseShift(-2.0f);
+ assertEquals(Math.PI * 2 - 2.0f, modifier.getPhaseShift(), 0.001);
+ }
+
+ @Test
+ public void testSetPhaseShiftZero() {
+ RippleModifier modifier = new RippleModifier();
+ modifier.setPhaseShift(0.0f);
+ assertEquals(0.0f, modifier.getPhaseShift(), 0.001);
+ }
+
+ @Test
+ public void testSetPhaseShiftLargePositive() {
+ RippleModifier modifier = new RippleModifier();
+ modifier.setPhaseShift(Mathf.PI * 10);
+ assertEquals(0.0f, modifier.getPhaseShift(), 0.001);
+ }
+
+ @Test
+ public void testSetPhaseShiftLargeNegative() {
+ RippleModifier modifier = new RippleModifier();
+ modifier.setPhaseShift(-Mathf.PI * 10);
+ assertEquals(0.0f, modifier.getPhaseShift(), 0.001);
+ }
+
+ @Test
+ public void testSetPhaseShiftExactlyTwoPi() {
+ modifier.setPhaseShift(Mathf.TWO_PI);
+ assertEquals(0.0f, modifier.getPhaseShift(), 0.001);
+ }
+
+ @Test
+ public void testSetPhaseShiftVerySmallPositive() {
+ modifier.setPhaseShift(0.0001f);
+ assertEquals(0.0001f, modifier.getPhaseShift(), 0.001);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/mesh/modifier/test/SpherifyModifierTest.java b/src/test/java/mesh/modifier/test/SpherifyModifierTest.java
new file mode 100644
index 00000000..caabdd17
--- /dev/null
+++ b/src/test/java/mesh/modifier/test/SpherifyModifierTest.java
@@ -0,0 +1,206 @@
+package mesh.modifier.test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import math.Mathf;
+import math.Vector3f;
+import mesh.Mesh3D;
+import mesh.creator.primitives.CubeCreator;
+import mesh.creator.primitives.IcoSphereCreator;
+import mesh.creator.primitives.SegmentedCubeCreator;
+import mesh.modifier.IMeshModifier;
+import mesh.modifier.SpherifyModifier;
+
+public class SpherifyModifierTest {
+
+ private SpherifyModifier modifier;
+
+ @BeforeEach
+ public void setUp() {
+ modifier = new SpherifyModifier();
+ }
+
+ @Test
+ public void testModifierImplementsMeshModifierInterface() {
+ assertTrue(modifier instanceof IMeshModifier);
+ }
+
+ @Test
+ public void testReturnsReferenceToModifedMesh() {
+ Mesh3D epected = new CubeCreator().create();
+ Mesh3D actual = modifier.modify(epected);
+ assertSame(epected, actual);
+ }
+
+ @Test
+ public void testDefaultConstructor() {
+ SpherifyModifier modifier = new SpherifyModifier();
+ assertEquals(1, modifier.getFactor());
+ assertEquals(1, modifier.getRadius());
+ assertEquals(Vector3f.ZERO, modifier.getCenter());
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { 2.45f, 0.2f, 100.345f, Float.MIN_VALUE, Float.MAX_VALUE })
+ public void testConstructorWithRadiusParameter() {
+ float expectedRadius = 2.55f;
+ SpherifyModifier modifier = new SpherifyModifier(expectedRadius);
+ assertEquals(1, modifier.getFactor());
+ assertEquals(expectedRadius, modifier.getRadius());
+ assertEquals(Vector3f.ZERO, modifier.getCenter());
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { 0, -0.1f, -100.3f, -Mathf.FLT_EPSILON })
+ public void testConstructorWithRadiusLessOrEqualsToZero(float radius) {
+ assertThrows(IllegalArgumentException.class, () -> new SpherifyModifier(radius));
+ }
+
+ @Test
+ public void testDefaultCenter() {
+ assertEquals(Vector3f.ZERO, modifier.getCenter());
+ }
+
+ @Test
+ public void testGetSetCenterViaParameters() {
+ float expectedX = 10.345f;
+ float expectedY = 345.553f;
+ float expectedZ = -1345.345f;
+ modifier.setCenter(expectedX, expectedY, expectedZ);
+ assertEquals(expectedX, modifier.getCenter().x);
+ assertEquals(expectedY, modifier.getCenter().y);
+ assertEquals(expectedZ, modifier.getCenter().z);
+ }
+
+ @Test
+ public void testGetSetCenterViaVector3f() {
+ Vector3f center = new Vector3f(0.134f, -23.443f, 100.0f);
+ modifier.setCenter(center);
+ assertEquals(center, modifier.getCenter());
+ }
+
+ @Test
+ public void testGetCenterReturnsImutable() {
+ Vector3f center = new Vector3f();
+ modifier.setCenter(center);
+ assertNotSame(center, modifier.getCenter());
+ }
+
+ @Test
+ public void testDefaultFactor() {
+ assertEquals(1.0f, modifier.getFactor());
+ }
+
+ @Test
+ public void testDefaultRadius() {
+ assertEquals(1, modifier.getRadius());
+ }
+
+ @Test
+ public void testModifyReturnsNonNullMesh() {
+ assertNotNull(modifier.modify(new CubeCreator().create()));
+ }
+
+ @Test
+ public void testModifyReturnsReferenceToModifiedMesh() {
+ Mesh3D expected = new CubeCreator().create();
+ Mesh3D actual = modifier.modify(expected);
+ assertSame(expected, actual);
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { -Float.MIN_VALUE, 0f, -1f, -Mathf.FLT_EPSILON })
+ public void testSetRadiusToLessOrEqualToZeroThrowsException(float radius) {
+ assertThrows(IllegalArgumentException.class, () -> modifier.setRadius(radius));
+ }
+
+ @Test
+ public void testSetNullCenterThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> modifier.setCenter(null));
+ }
+
+ @Test
+ public void testModifyNullMeshThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> modifier.modify(null));
+ }
+
+ @Test
+ public void testSetFactorAboveBoundsThrowsException() {
+ float factor = 1.0f + Mathf.FLT_EPSILON;
+ assertThrows(IllegalArgumentException.class, () -> modifier.setFactor(factor));
+ }
+
+ @Test
+ public void testSetFactorBelowBoundsThrowsException() {
+ float factor = -Mathf.FLT_EPSILON;
+ assertThrows(IllegalArgumentException.class, () -> modifier.setFactor(factor));
+ }
+
+ @Test
+ public void testGetSetFactorWithinBouns() {
+ float expectedFactor = 0.126f;
+ modifier.setFactor(expectedFactor);
+ assertEquals(expectedFactor, modifier.getFactor());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 2, 4, 10, 20 })
+ public void testMultipleIteractionsHaveSameEffectAsOne(int n) {
+ Mesh3D expected = new CubeCreator().create();
+ Mesh3D actual = new CubeCreator().create();
+ modifier.modify(expected);
+ for (int i = 0; i < n; i++) {
+ modifier.modify(actual);
+ }
+ for (int i = 0; i < expected.getVertexCount(); i++) {
+ Vector3f expectedVertex = expected.getVertexAt(i);
+ Vector3f actualVertex = actual.getVertexAt(i);
+ assertEquals(expectedVertex.x, actualVertex.x, 0.001f);
+ assertEquals(expectedVertex.y, actualVertex.y, 0.001f);
+ assertEquals(expectedVertex.z, actualVertex.z, 0.001f);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(floats = { 2.134f, 4.234f, Float.MIN_VALUE })
+ public void testMultipleRadiiWithFactorOne(float expectedRadius) {
+ int segments = 30;
+ float size = 1.0f;
+ SegmentedCubeCreator creator = new SegmentedCubeCreator(segments, size);
+ Mesh3D mesh = creator.create();
+ modifier.setRadius(expectedRadius);
+ modifier.modify(mesh);
+ for (Vector3f vertex : mesh.vertices) {
+ float distance = vertex.distance(Vector3f.ZERO);
+ assertEquals(expectedRadius, distance, 0.001f);
+ }
+ }
+
+ @Test
+ public void testZeroFactorLeavesTheMeshUnchanged() {
+ IcoSphereCreator creator = new IcoSphereCreator(1, 3);
+ Mesh3D expected = creator.create();
+ Mesh3D actual = creator.create();
+ modifier.setFactor(0);
+ modifier.setRadius(3);
+ modifier.modify(actual);
+ for (int i = 0; i < expected.getVertexCount(); i++) {
+ Vector3f expectedVertex = expected.getVertexAt(i);
+ Vector3f actualVertex = actual.getVertexAt(i);
+ assertEquals(expectedVertex.x, actualVertex.x, 0.001f);
+ assertEquals(expectedVertex.y, actualVertex.y, 0.001f);
+ assertEquals(expectedVertex.z, actualVertex.z, 0.001f);
+ }
+ }
+
+}
\ No newline at end of file