diff --git a/.gitignore b/.gitignore index 965fe30..e8898fe 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,7 @@ /src/test/skriptparticle/ CLAUDE.md /.claude -/shapes-lib/build -/skript-particle/build +/shapes-lib/build/ +/skript-particle/build/ +/skript-particle/.gradle/ +/net/minecraft/network diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927..d64cd49 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a595206..37f78a6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..1aa94a4 100644 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 27f5d3b..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,10 +14,10 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem Gradle startup script for Windows @rem @rem ########################################################################## @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle b/settings.gradle index bd0f6f2..818aba4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,11 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + url 'https://repo.papermc.io/repository/maven-public/' + } + } +} + rootProject.name = 'skript-particle-root' include 'shapes-lib', 'skript-particle' diff --git a/shapes-lib/build.gradle b/shapes-lib/build.gradle index b5aef52..d377622 100644 --- a/shapes-lib/build.gradle +++ b/shapes-lib/build.gradle @@ -10,6 +10,13 @@ repositories { dependencies { api "org.joml:joml:${jomlVersion}" + compileOnly 'org.jetbrains:annotations:24.0.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() } java { diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/Easable.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/Easable.java new file mode 100644 index 0000000..cde3f41 --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/Easable.java @@ -0,0 +1,18 @@ +package com.sovdee.shapes.modifiers; + +/** + * Something that has an {@link EasingFunction}. + */ +public interface Easable { + + /** + * @return the easing function used to distribute rotation along the axis + */ + EasingFunction getEasing(); + + /** + * @param easing the easing function used to distribute rotation along the axis + */ + void setEasing(EasingFunction easing); + +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/EasingFunction.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/EasingFunction.java new file mode 100644 index 0000000..ff4a985 --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/EasingFunction.java @@ -0,0 +1,165 @@ +package com.sovdee.shapes.modifiers; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +/** + * Maps a normalised value {@code t ∈ [0, 1]} to a (typically) {@code [0, 1]} output, shaping + * how interpolation progresses along a modifier's input axis. + *
+ * Built-in constants and factory methods cover the most common curves, like {@link #SINE_IN_OUT}. + *
+ * Custom implementations must provide a stable {@link #easingHash()} so that modifier cache + * invalidation works correctly. + */ +public interface EasingFunction { + + /** + * Given an input between 0 and 1, applies the easing function and returns a mapped output + * @param t Input between 0 and 1. + * @return Output value mapped by the easing function. + */ + double apply(double t); + + /** + * @return Stable hash of this easing's configuration, used in modifier cache keys. + * Two easings that produce identical output for all inputs should return the same value. + */ + int easingHash(); + + /** + * Controls which end of the interpolation range is eased. + * + */ + enum Mode { + IN, + OUT, + IN_OUT + } + + /** + * Power-curve easing: {@code t^n} (in), {@code 1-(1-t)^n} (out), + * symmetric combination (in-out). + * @param exponent 1 = linear, 2 = quadratic, 3 = cubic, etc. + * @param mode Whether to have the start, end, or both be smooth. + */ + record PowerEasing(double exponent, Mode mode) implements EasingFunction { + /** + * Applies the power-curve easing to {@code t}. + * + * + * @param t normalised input in {@code [0, 1]} + * @return eased output value + */ + @Override + public double apply(double t) { + return switch (mode) { + case IN -> Math.pow(t, exponent); + case OUT -> 1.0 - Math.pow(1.0 - t, exponent); + case IN_OUT -> { + if (t < 0.5) + yield Math.pow(2.0 * t, exponent) / 2.0; + yield 1.0 - Math.pow(-2.0 * t + 2.0, exponent) / 2.0; + } + }; + } + + /** + * @return a hash combining the exponent and mode ordinal, + * unique among all standard {@code PowerEasing} configurations + */ + @Override + public int easingHash() { + return 31 * Double.hashCode(exponent) + mode.ordinal(); + } + } + + /** + * Sine-based easing. + * @param mode Whether to have the start, end, or both be smooth. + */ + record SineEasing(Mode mode) implements EasingFunction { + /** + * Applies the sine-based easing to {@code t}. + * + * + * @param t normalised input in {@code [0, 1]} + * @return eased output value + */ + @Override + public double apply(double t) { + return switch (mode) { + case IN -> 1.0 - Math.cos(t * Math.PI / 2.0); + case OUT -> Math.sin(t * Math.PI / 2.0); + case IN_OUT -> -(Math.cos(Math.PI * t) - 1.0) / 2.0; + }; + } + + @Override + public int easingHash() { + return 1000 + mode.ordinal(); + } + } + + // ------------------------------------------------------------------------- + // Named constants + // ------------------------------------------------------------------------- + + EasingFunction LINEAR = new PowerEasing(1.0, Mode.IN); + + EasingFunction QUAD_IN = new PowerEasing(2.0, Mode.IN); + EasingFunction QUAD_OUT = new PowerEasing(2.0, Mode.OUT); + EasingFunction QUAD_IN_OUT = new PowerEasing(2.0, Mode.IN_OUT); + + EasingFunction CUBIC_IN = new PowerEasing(3.0, Mode.IN); + EasingFunction CUBIC_OUT = new PowerEasing(3.0, Mode.OUT); + EasingFunction CUBIC_IN_OUT = new PowerEasing(3.0, Mode.IN_OUT); + + EasingFunction SINE_IN = new SineEasing(Mode.IN); + EasingFunction SINE_OUT = new SineEasing(Mode.OUT); + EasingFunction SINE_IN_OUT = new SineEasing(Mode.IN_OUT); + + // ------------------------------------------------------------------------- + // Factory methods + // ------------------------------------------------------------------------- + + /** + * @param exponent Degree to use for the easing function + * @return A power-based easing function with a gradual start and abrupt end + */ + @Contract("_ -> new") + static @NotNull EasingFunction powerIn(double exponent) { + return new PowerEasing(exponent, Mode.IN); + } + + /** + * @param exponent Degree to use for the easing function + * @return A power-based easing function with an abrupt start and gradual end + */ + @Contract("_ -> new") + static @NotNull EasingFunction powerOut(double exponent) { + return new PowerEasing(exponent, Mode.OUT); + } + + /** + * @param exponent Degree to use for the easing function + * @return A power-based easing function with a gradual start and end + */ + @Contract("_ -> new") + static @NotNull EasingFunction powerInOut(double exponent) { + return new PowerEasing(exponent, Mode.IN_OUT); + } + +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/HasInput.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/HasInput.java new file mode 100644 index 0000000..4d158d1 --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/HasInput.java @@ -0,0 +1,17 @@ +package com.sovdee.shapes.modifiers; + +/** + * A modifier that is driven by a {@link NormalizedInput}. + */ +public interface HasInput { + + /** + * @return the input that drives this modifier + */ + NormalizedInput getInput(); + + /** + * @param input the input to drive this modifier with + */ + void setInput(NormalizedInput input); +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/NormalizedInput.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/NormalizedInput.java new file mode 100644 index 0000000..80ad5ba --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/NormalizedInput.java @@ -0,0 +1,23 @@ +package com.sovdee.shapes.modifiers; + +/** + * A normalized input source for driving per-point modifier computations. + * Returns a value in [0, 1] derived from the point's current context. + */ +public interface NormalizedInput { + + /** + * Returns a value in [0, 1] derived from the point's current context. + * + * @param point the current point context + * @return a value in [0, 1] + */ + double sample(PointContext point); + + /** + * Stable hash of this input's identity, for use in {@code modifierHash()}. + * + * @return a stable integer hash + */ + int inputHash(); +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/PointContext.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/PointContext.java new file mode 100644 index 0000000..d3c0640 --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/PointContext.java @@ -0,0 +1,84 @@ +package com.sovdee.shapes.modifiers; + +import com.sovdee.shapes.shapes.Shape; + +/** + * Geometry-only context for a single point during modifier and render passes. + * A single instance is allocated per draw call and reused across all points. + * + *

Clients that need render-specific properties (color, motion, visibility) should + * extend this class and override {@link #reset()} to clear their additional fields. + */ +public class PointContext { + + /** + * Shape reference. + */ + public Shape shape; + + /** + * Local-space position (writable by geometry modifiers). + */ + public double x, y, z; + + /** + * 0-based index of this point in the draw order. + */ + public int index; + + /** + * Total number of points being drawn this pass. + */ + public int totalPoints; + + /** + * Pre-computed bounds for normalization helpers. Set once per pass before the modify loop. + */ + public ShapeBounds bounds; + + // ---- Normalization convenience (delegate to bounds) ---- + + /** + * @return Normalised X position within shape bounds [0, 1]. + */ + public double normalizedX() { return bounds.normalizeX(x); } + + /** + * @return Normalised Y position within shape bounds [0, 1]. + */ + public double normalizedY() { return bounds.normalizeY(y); } + + /** + * @return Normalised Z position within shape bounds [0, 1]. + */ + public double normalizedZ() { return bounds.normalizeZ(z); } + + /** + * @return Normalised XZ radius [0, 1] relative to max XZ extent. + */ + public double normalizedRadius() { return bounds.normalizeRadial(x, z); } + + /** + * @return Normalised 3D spherical radius [0, 1] relative to max XZ radius. + */ + public double normalizedSpherical() { + double r = Math.sqrt(x * x + y * y + z * z); + double maxR = bounds.maxRadiusXZ(); + return maxR > 1e-12 ? r / maxR : 0.0; + } + + /** + * @return XZ angle mapped to [0, 1] (0 = +X axis, wraps around). + */ + public double angle() { + double a = Math.atan2(z, x); + if (a < 0) a += 2 * Math.PI; + return a / (2 * Math.PI); + } + + /** + * Resets mutable (non-geometry) fields between points. + * Override in subclasses to clear render-specific properties (color, motion, etc.). + */ + public void reset() {} +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/PointModifier.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/PointModifier.java new file mode 100644 index 0000000..0bdecf1 --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/PointModifier.java @@ -0,0 +1,78 @@ +package com.sovdee.shapes.modifiers; + +import com.sovdee.shapes.sampling.DefaultPointSampler; + +/** + * Transforms individual points. Geometry modifiers (taper, twist, wave) transform position + * in local space before orientation/scale and are cached with shape state. + *
+ * Modifiers that can run prior to rendering (geometry-only modifiers) should extends {@link PreRenderPointModifier} + */ +public interface PointModifier extends Cloneable { + + /** + * Called once per pass before the {@link #modify} loop. + * Pre-compute expensive state here (e.g., LUTs, inverse ranges). + * + * @param bounds the bounding box of the points for this pass + */ + default void prepare(ShapeBounds bounds) {} + + /** + * Called per point. Avoid allocations in this method. + * Modify the provided context to modify the point/render. + * + * @param point mutable per-point context, reused. Do not hold references. + */ + void modify(Context point); + + /** + * Returns a hash code representing this modifier's current configuration. + * Used for cache invalidation in {@link DefaultPointSampler}. + */ + int modifierHash(); + + /** + * Returns a deep copy of this modifier. The clone must be fully independent: + * changes to the clone's configuration must not affect the original. + * + * @return a new modifier instance with the same configuration + */ + PointModifier clone(); + + /** + * The minimum context type this modifier requires. + * Defaults to {@link PointContext} (compatible with any renderer). + * Render modifiers should override this to declare their specific required type, + * enabling validation in {@link DefaultPointSampler#render}. + */ + default Class contextType() { + return PointContext.class; + } + + /** + * Marker base class for geometry-only (pre-render) modifiers. + * These run during point sampling and are cached with the shape state. + * They always operate on plain {@link PointContext}. + */ + abstract class PreRenderPointModifier implements PointModifier { + + /** + * Returns a shallow clone of this geometry modifier. + * Sufficient for modifiers whose fields are all primitives or immutable objects. + * + * @return a new {@code PointModifier} instance with the same configuration + * @throws RuntimeException wrapping {@link CloneNotSupportedException} if cloning fails + */ + @Override + @SuppressWarnings("unchecked") + public PointModifier clone() { + try { + return (PointModifier) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + } +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/RotationPlane.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/RotationPlane.java new file mode 100644 index 0000000..811bdcf --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/RotationPlane.java @@ -0,0 +1,21 @@ +package com.sovdee.shapes.modifiers; + +/** + * The plane in which a {@link TwistModifier} rotates points. + */ +public enum RotationPlane { + /** + * rotates around the Y axis, acts on X and Z + */ + XZ, + + /** + * rotates around the Z axis, acts on X and Y + */ + XY, + + /** + * rotates around the X axis, acts on Y and Z + */ + YZ +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ScaleAxes.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ScaleAxes.java new file mode 100644 index 0000000..9639810 --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ScaleAxes.java @@ -0,0 +1,54 @@ +package com.sovdee.shapes.modifiers; + +/** + * Specifies which coordinate axes are scaled by a {@link ScalingModifier}. + */ +public enum ScaleAxes { + /** + * Scales the X axis only. + */ + X (true, false, false), + + /** + * Scales the Y axis only. + */ + Y (false, true, false), + + /** + * Scales the Z axis only. + */ + Z (false, false, true), + + /** + * Scales the X and Y axes. + */ + XY (true, true, false), + + /** + * Scales the X and Z axes. + */ + XZ (true, false, true), + + /** + * Scales the Y and Z axes. + */ + YZ (false, true, true), + + /** + * Scales all three axes. + */ + XYZ (true, true, true); + + /** Whether the X axis is scaled. */ + public final boolean x; + /** Whether the Y axis is scaled. */ + public final boolean y; + /** Whether the Z axis is scaled. */ + public final boolean z; + + ScaleAxes(boolean x, boolean y, boolean z) { + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ScalingModifier.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ScalingModifier.java new file mode 100644 index 0000000..c31d8c4 --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ScalingModifier.java @@ -0,0 +1,148 @@ +package com.sovdee.shapes.modifiers; + +/** + * Scales any combination of axes by a value that linearly interpolates from + * {@code startScale} to {@code endScale} based on a {@link NormalizedInput}. + * + *

Examples:

+ * + */ +public class ScalingModifier extends PointModifier.PreRenderPointModifier implements HasInput, Easable { + + private double startScale; + private double endScale; + private NormalizedInput input; + private ScaleAxes axes; + private EasingFunction easing = EasingFunction.LINEAR; + + /** + * Creates a {@code ScalingModifier} that scales the XZ plane driven by the Y axis. + * + * @param startScale scale factor at input minimum + * @param endScale scale factor at input maximum + */ + public ScalingModifier(double startScale, double endScale) { + this(startScale, endScale, StandardInput.Y, ScaleAxes.XZ); + } + + /** + * Creates a {@code ScalingModifier} with a specified input and set of scaled axes. + * + * @param startScale scale factor at input minimum + * @param endScale scale factor at input maximum + * @param input the normalized input that drives the scale factor + * @param axes which axes to scale + */ + public ScalingModifier(double startScale, double endScale, NormalizedInput input, ScaleAxes axes) { + this.startScale = startScale; + this.endScale = endScale; + this.input = input; + this.axes = axes; + } + + /** + * Scales the configured axes by {@code startScale + easing(t) * (endScale - startScale)}, + * where {@code t} is sampled from the input. + * + * @param point the mutable point context to transform + */ + @Override + public void modify(PointContext point) { + double t = easing.apply(input.sample(point)); + double scale = startScale + t * (endScale - startScale); + if (axes.x) point.x *= scale; + if (axes.y) point.y *= scale; + if (axes.z) point.z *= scale; + } + + /** + * {@inheritDoc} + * Includes {@code startScale}, {@code endScale}, {@code input}, {@code axes}, + * and the easing hash. + */ + @Override + public int modifierHash() { + int result = Double.hashCode(startScale); + result = 31 * result + Double.hashCode(endScale); + result = 31 * result + input.inputHash(); + result = 31 * result + axes.ordinal(); + result = 31 * result + easing.easingHash(); + return result; + } + + /** + * @return the scale factor at input minimum + */ + public double getStartScale() { + return startScale; + } + + /** + * @param startScale the scale factor at input minimum + */ + public void setStartScale(double startScale) { + this.startScale = startScale; + } + + /** + * @return the scale factor at input maximum + */ + public double getEndScale() { + return endScale; + } + + /** + * @param endScale the scale factor at input maximum + */ + public void setEndScale(double endScale) { + this.endScale = endScale; + } + + /** + * @return which axes are scaled + */ + public ScaleAxes getAxes() { + return axes; + } + + /** + * @param axes which axes to scale + */ + public void setAxes(ScaleAxes axes) { + this.axes = axes; + } + + @Override + public NormalizedInput getInput() { + return input; + } + + @Override + public void setInput(NormalizedInput input) { + this.input = input; + } + + @Override + public EasingFunction getEasing() { + return easing; + } + + @Override + public void setEasing(EasingFunction easing) { + this.easing = easing; + } + + /** + * Returns a deep copy of this modifier with independent state. + * + * @return a new {@code ScalingModifier} with the same configuration + */ + @Override + public ScalingModifier clone() { + return (ScalingModifier) super.clone(); + } +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ShapeBounds.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ShapeBounds.java new file mode 100644 index 0000000..d658c69 --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/ShapeBounds.java @@ -0,0 +1,190 @@ +package com.sovdee.shapes.modifiers; + +import org.joml.Vector3d; + +import java.util.List; + +/** + * Axis-aligned bounding box computed from a set of points. + * Provides normalization helpers for use in modifiers and shaders. + */ +public final class ShapeBounds { + + private final double minX, minY, minZ; + private final double maxX, maxY, maxZ; + private final double rangeX, rangeY, rangeZ; + private final double maxRadius; + + // Pre-computed inverse ranges for fast normalization (no division in hot loop) + private final double invRangeX, invRangeY, invRangeZ, invMaxRadius; + + private ShapeBounds(double minX, double minY, double minZ, + double maxX, double maxY, double maxZ, + double maxRadius) { + this.minX = minX; this.minY = minY; this.minZ = minZ; + this.maxX = maxX; this.maxY = maxY; this.maxZ = maxZ; + this.rangeX = maxX - minX; + this.rangeY = maxY - minY; + this.rangeZ = maxZ - minZ; + this.maxRadius = maxRadius; + this.invRangeX = rangeX > 1e-6 ? 1.0 / rangeX : 0.0; + this.invRangeY = rangeY > 1e-6 ? 1.0 / rangeY : 0.0; + this.invRangeZ = rangeZ > 1e-6 ? 1.0 / rangeZ : 0.0; + this.invMaxRadius = maxRadius > 1e-6 ? 1.0 / maxRadius : 0.0; + } + + /** + * Computes the bounding box from the given list of points. + * Returns a degenerate (all-zero) bounds if the list is empty. + */ + public static ShapeBounds compute(List points) { + if (points.isEmpty()) { + return new ShapeBounds(0, 0, 0, 0, 0, 0, 0); + } + double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE, minZ = Double.MAX_VALUE; + double maxX = -Double.MAX_VALUE, maxY = -Double.MAX_VALUE, maxZ = -Double.MAX_VALUE; + double maxRadius = 0; + for (Vector3d p : points) { + if (p.x < minX) minX = p.x; + if (p.y < minY) minY = p.y; + if (p.z < minZ) minZ = p.z; + if (p.x > maxX) maxX = p.x; + if (p.y > maxY) maxY = p.y; + if (p.z > maxZ) maxZ = p.z; + double r = Math.sqrt(p.x * p.x + p.z * p.z); + if (r > maxRadius) maxRadius = r; + } + return new ShapeBounds(minX, minY, minZ, maxX, maxY, maxZ, maxRadius); + } + + /** + * Normalizes x to [0, 1] within the shape's X extent. + */ + public double normalizeX(double x) { + return (x - minX) * invRangeX; + } + + /** + * Normalizes y to [0, 1] within the shape's Y extent. + */ + public double normalizeY(double y) { + return (y - minY) * invRangeY; + } + + /** + * Normalizes z to [0, 1] within the shape's Z extent. + */ + public double normalizeZ(double z) { + return (z - minZ) * invRangeZ; + } + + /** + * Normalizes the XZ radius to [0, 1] relative to the shape's max XZ radius. + */ + public double normalizeRadial(double x, double z) { + return Math.sqrt(x * x + z * z) * invMaxRadius; + } + + /** + * Returns the max XZ distance from origin across all points. + */ + public double maxRadiusXZ() { + return maxRadius; + } + + /** + * @return the extent of the bounding box along the Y axis ({@code maxY - minY}) + */ + public double height() { + return rangeY; + } + /** + * @return the extent of the bounding box along the X axis ({@code maxX - minX}) + */ + public double width() { + return rangeX; + } + /** + * @return the extent of the bounding box along the Z axis ({@code maxZ - minZ}) + */ + public double length() { + return rangeZ; + } + + /** + * @return the minimum X coordinate across all points + */ + public double minX() { + return minX; + } + /** + * @return the minimum Y coordinate across all points + */ + public double minY() { + return minY; + } + /** + * @return the minimum Z coordinate across all points + */ + public double minZ() { + return minZ; + } + /** + * @return the maximum X coordinate across all points + */ + public double maxX() { + return maxX; + } + /** + * @return the maximum Y coordinate across all points + */ + public double maxY() { + return maxY; + } + /** + * @return the maximum Z coordinate across all points + */ + public double maxZ() { + return maxZ; + } + + /** + * Pre-computed reciprocal of the X extent, used for fast normalisation without division. + * Returns {@code 0} if the X extent is degenerate (less than {@code 1e-12}). + * + * @return {@code 1 / rangeX}, or {@code 0} if the range is effectively zero + */ + public double invRangeX() { + return invRangeX; + } + + /** + * Pre-computed reciprocal of the Y extent, used for fast normalisation without division. + * Returns {@code 0} if the Y extent is degenerate (less than {@code 1e-12}). + * + * @return {@code 1 / rangeY}, or {@code 0} if the range is effectively zero + */ + public double invRangeY() { + return invRangeY; + } + + /** + * Pre-computed reciprocal of the Z extent, used for fast normalisation without division. + * Returns {@code 0} if the Z extent is degenerate (less than {@code 1e-12}). + * + * @return {@code 1 / rangeZ}, or {@code 0} if the range is effectively zero + */ + public double invRangeZ() { + return invRangeZ; + } + + /** + * Pre-computed reciprocal of the maximum XZ radius, used for fast radial normalisation. + * Returns {@code 0} if the maximum radius is degenerate (less than {@code 1e-12}). + * + * @return {@code 1 / maxRadius}, or {@code 0} if the radius is effectively zero + */ + public double invMaxRadius() { + return invMaxRadius; + } +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/SpatialAxis.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/SpatialAxis.java new file mode 100644 index 0000000..b5f72bc --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/SpatialAxis.java @@ -0,0 +1,22 @@ +package com.sovdee.shapes.modifiers; + +/** + * A spatial coordinate axis. + * Used to specify the output displacement axis of a {@link WaveModifier}. + */ +public enum SpatialAxis { + /** + * The X axis. + */ + X, + + /** + * The Y axis. + */ + Y, + + /** + * The Z axis. + */ + Z +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/StandardInput.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/StandardInput.java new file mode 100644 index 0000000..f53bf6e --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/StandardInput.java @@ -0,0 +1,71 @@ +package com.sovdee.shapes.modifiers; + +/** + * Standard normalized inputs for driving geometry modifiers. + * Each constant samples a specific aspect of the point's context, + * returning a value in [0, 1]. + */ +public enum StandardInput implements NormalizedInput { + /** + * Normalized position within the X extent of the shape's bounds. + */ + X { + @Override + public double sample(PointContext point) { return point.normalizedX(); } + }, + + /** + * Normalized position within the Y extent of the shape's bounds. + */ + Y { + @Override + public double sample(PointContext point) { return point.normalizedY(); } + }, + + /** + * Normalized position within the Z extent of the shape's bounds. + */ + Z { + @Override + public double sample(PointContext point) { return point.normalizedZ(); } + }, + + /** + * Normalized draw order: {@code index / totalPoints}. + */ + T { + @Override + public double sample(PointContext point) { + return point.totalPoints > 0 ? (double) point.index / point.totalPoints : 0.0; + } + }, + + /** + * XZ distance from the origin, normalized to the maximum XZ radius. + */ + RADIUS { + @Override + public double sample(PointContext point) { return point.normalizedRadius(); } + }, + + /** + * 3D distance from the origin, normalized to the maximum XZ radius. + */ + SPHERICAL { + @Override + public double sample(PointContext point) { return point.normalizedSpherical(); } + }, + + /** + * XZ angle mapped to [0, 1] over a full revolution (0 = +X axis). + */ + ANGLE { + @Override + public double sample(PointContext point) { return point.angle(); } + }; + + @Override + public int inputHash() { + return ordinal(); + } +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/TwistModifier.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/TwistModifier.java new file mode 100644 index 0000000..df3d75e --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/TwistModifier.java @@ -0,0 +1,147 @@ +package com.sovdee.shapes.modifiers; + +/** + * Rotates each point in the specified {@link RotationPlane} by an angle proportional to + * the value of a {@link NormalizedInput}. A {@code totalAngle} of 2π produces one full twist. + * + *

Examples:

+ * + */ +public class TwistModifier extends PointModifier.PreRenderPointModifier implements HasInput, Easable { + + private double totalAngle; + private NormalizedInput input; + private RotationPlane plane; + private EasingFunction easing = EasingFunction.LINEAR; + + /** + * Creates a {@code TwistModifier} in the XZ plane driven by the Y axis. + * + * @param totalAngle total rotation in radians; {@code 2π} produces one full revolution + */ + public TwistModifier(double totalAngle) { + this(totalAngle, StandardInput.Y, RotationPlane.XZ); + } + + /** + * Creates a {@code TwistModifier} with a specified input and rotation plane. + * + * @param totalAngle total rotation in radians; {@code 2π} produces one full revolution + * @param input the normalized input that drives the rotation angle + * @param plane the plane in which points are rotated + */ + public TwistModifier(double totalAngle, NormalizedInput input, RotationPlane plane) { + this.totalAngle = totalAngle; + this.input = input; + this.plane = plane; + } + + /** + * Rotates the point in the configured plane by {@code easing(t) * totalAngle} radians, + * where {@code t} is sampled from the input. + * + * @param point the mutable point context to transform + */ + @Override + public void modify(PointContext point) { + double t = easing.apply(input.sample(point)); + double angle = totalAngle * t; + double cos = Math.cos(angle); + double sin = Math.sin(angle); + switch (plane) { + case YZ -> { + double newY = point.y * cos - point.z * sin; + double newZ = point.y * sin + point.z * cos; + point.y = newY; + point.z = newZ; + } + case XY -> { + double newX = point.x * cos - point.y * sin; + double newY = point.x * sin + point.y * cos; + point.x = newX; + point.y = newY; + } + default -> { // XZ + double newX = point.x * cos - point.z * sin; + double newZ = point.x * sin + point.z * cos; + point.x = newX; + point.z = newZ; + } + } + } + + /** + * {@inheritDoc} + * Includes {@code totalAngle}, {@code input}, {@code plane}, and the easing hash. + */ + @Override + public int modifierHash() { + int result = Double.hashCode(totalAngle); + result = 31 * result + input.inputHash(); + result = 31 * result + plane.ordinal(); + result = 31 * result + easing.easingHash(); + return result; + } + + /** + * @return the total rotation angle in radians + */ + public double getTotalAngle() { + return totalAngle; + } + + /** + * @param totalAngle the total rotation angle in radians + */ + public void setTotalAngle(double totalAngle) { + this.totalAngle = totalAngle; + } + + /** + * @return the rotation plane + */ + public RotationPlane getPlane() { + return plane; + } + + /** + * @param plane the rotation plane + */ + public void setPlane(RotationPlane plane) { + this.plane = plane; + } + + @Override + public NormalizedInput getInput() { + return input; + } + + @Override + public void setInput(NormalizedInput input) { + this.input = input; + } + + @Override + public EasingFunction getEasing() { + return easing; + } + + @Override + public void setEasing(EasingFunction easing) { + this.easing = easing; + } + + /** + * Returns a deep copy of this modifier with independent state. + * + * @return a new {@code TwistModifier} with the same configuration + */ + @Override + public TwistModifier clone() { + return (TwistModifier) super.clone(); + } +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/WaveModifier.java b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/WaveModifier.java new file mode 100644 index 0000000..d9be7ce --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/modifiers/WaveModifier.java @@ -0,0 +1,163 @@ +package com.sovdee.shapes.modifiers; + +/** + * Displaces points by {@code amplitude * sin(2π * frequency * input + phase)}, + * where input and output are independently configurable. + * + *

{@code frequency} is expressed in cycles over the full [0, 1] input range, + * so {@code frequency=1} always means exactly one full sine cycle regardless of + * shape size or which input is used.

+ * + *

Output axis: the coordinate component to displace (X, Y, or Z).

+ *

Input: any {@link NormalizedInput} — X, Y, Z, T, RADIUS, SPHERICAL, or ANGLE.

+ */ +public class WaveModifier extends PointModifier.PreRenderPointModifier { + + private double amplitude; + private double frequency; + private double phase; + private NormalizedInput input; + private SpatialAxis outputAxis; + + /** + * Creates a fully configured wave modifier. + * + * @param amplitude peak displacement distance (positive or negative) + * @param frequency cycles over the full [0, 1] input range; {@code 1} = one full cycle + * @param phase phase offset in radians + * @param input the normalized input used as the sine function's argument + * @param outputAxis the spatial coordinate component to displace + */ + public WaveModifier(double amplitude, double frequency, double phase, + NormalizedInput input, SpatialAxis outputAxis) { + this.amplitude = amplitude; + this.frequency = frequency; + this.phase = phase; + this.input = input; + this.outputAxis = outputAxis; + } + + /** + * Creates a wave displacing {@code outputAxis} driven by normalized draw order (T), + * with a phase of {@code 0}. + * + * @param amplitude peak displacement distance + * @param frequency cycles over the full [0, 1] T range + * @param outputAxis the spatial coordinate component to displace + */ + public WaveModifier(double amplitude, double frequency, SpatialAxis outputAxis) { + this(amplitude, frequency, 0.0, StandardInput.T, outputAxis); + } + + /** + * Displaces the point along {@code outputAxis} by + * {@code amplitude * sin(2π * frequency * input + phase)}, + * where {@code input} is sampled from the configured {@link NormalizedInput}. + * + * @param point the mutable point context to transform + */ + @Override + public void modify(PointContext point) { + double displacement = amplitude * Math.sin(2 * Math.PI * frequency * input.sample(point) + phase); + switch (outputAxis) { + case X -> point.x += displacement; + case Y -> point.y += displacement; + case Z -> point.z += displacement; + } + } + + /** + * {@inheritDoc} + * Includes {@code amplitude}, {@code frequency}, {@code phase}, {@code input}, + * and {@code outputAxis}. + */ + @Override + public int modifierHash() { + int result = Double.hashCode(amplitude); + result = 31 * result + Double.hashCode(frequency); + result = 31 * result + Double.hashCode(phase); + result = 31 * result + input.inputHash(); + result = 31 * result + outputAxis.ordinal(); + return result; + } + + /** + * @return the peak displacement distance + */ + public double getAmplitude() { + return amplitude; + } + + /** + * @param amplitude the peak displacement distance + */ + public void setAmplitude(double amplitude) { + this.amplitude = amplitude; + } + + /** + * @return the number of cycles over the full [0, 1] input range + */ + public double getFrequency() { + return frequency; + } + + /** + * @param frequency the number of cycles over the full [0, 1] input range + */ + public void setFrequency(double frequency) { + this.frequency = frequency; + } + + /** + * @return the phase offset in radians + */ + public double getPhase() { + return phase; + } + + /** + * @param phase the phase offset in radians + */ + public void setPhase(double phase) { + this.phase = phase; + } + + /** + * @return the normalized input driving the wave + */ + public NormalizedInput getInput() { + return input; + } + + /** + * @param input the normalized input to drive the wave with + */ + public void setInput(NormalizedInput input) { + this.input = input; + } + + /** + * @return the spatial coordinate component that is displaced + */ + public SpatialAxis getOutputAxis() { + return outputAxis; + } + + /** + * @param outputAxis the spatial coordinate component to displace + */ + public void setOutputAxis(SpatialAxis outputAxis) { + this.outputAxis = outputAxis; + } + + /** + * Returns a deep copy of this modifier with independent state. + * + * @return a new {@code WaveModifier} with the same configuration + */ + @Override + public WaveModifier clone() { + return (WaveModifier) super.clone(); + } +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/DefaultPointSampler.java b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/DefaultPointSampler.java index cd76bac..3272f4e 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/DefaultPointSampler.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/DefaultPointSampler.java @@ -1,13 +1,16 @@ package com.sovdee.shapes.sampling; +import com.sovdee.shapes.modifiers.PointContext; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.shapes.modifiers.PointModifier.PreRenderPointModifier; +import com.sovdee.shapes.modifiers.ShapeBounds; import com.sovdee.shapes.shapes.Shape; import org.joml.Quaterniond; import org.joml.Vector3d; +import java.util.ArrayList; import java.util.Comparator; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.TreeSet; +import java.util.List; import java.util.UUID; /** @@ -15,47 +18,116 @@ */ public class DefaultPointSampler implements PointSampler { + private static final int DEFAULT_MAX_POINTS = 10_000; + private SamplingStyle style = SamplingStyle.OUTLINE; private double density = 0.25; + private int maxPoints = DEFAULT_MAX_POINTS; + private boolean densityExplicit = false; private Comparator ordering; private final UUID uuid; private DrawContext drawContext; + private List> modifiers = new ArrayList<>(); // Cache - private Set cachedPoints = new LinkedHashSet<>(); + private List cachedPoints = new ArrayList<>(); private CacheState lastState; private boolean needsUpdate = false; + /** + * Creates a new {@code DefaultPointSampler} with default settings: + * {@link SamplingStyle#OUTLINE}, density {@code 0.25}, max points {@code 10 000}, + * no ordering, no modifiers, and an empty point cache. + * A fresh {@link UUID} is assigned at construction time. + */ public DefaultPointSampler() { this.uuid = UUID.randomUUID(); - this.lastState = new CacheState(style, 0, 1.0, 0, density, 0); + this.lastState = new CacheState(style, 0, 1.0, 0, density, 0, 0); } + + /** + * Returns sampled points for the given shape using the shape's own orientation. + * Results are cached and only recomputed when the shape or sampler state changes. + * + * @param shape the shape to sample + * @return an ordered list of transformed world-space points + */ @Override - public Set getPoints(Shape shape) { + public List getPoints(Shape shape) { return getPoints(shape, shape.getOrientation()); } + /** + * Returns sampled points for the given shape using the supplied orientation quaternion. + * The cache is invalidated when the style, orientation, scale, offset, density, + * shape version, or geometry-modifier configuration changes. + * Points are transformed (orientation → scale → offset) before being returned. + * + * @param shape the shape to sample + * @param orientation the orientation to apply to the sampled points + * @return an ordered list of transformed world-space points + */ @Override - public Set getPoints(Shape shape, Quaterniond orientation) { + public List getPoints(Shape shape, Quaterniond orientation) { + double workingDensity; + if (densityExplicit) { + workingDensity = density; + } else { + double auto = shape.computeDensity(style, maxPoints); + workingDensity = (auto > Shape.EPSILON) ? auto : density; + } + CacheState state = new CacheState(style, orientation.hashCode(), - shape.getScale(), shape.getOffset().hashCode(), density, shape.getVersion()); + shape.getScale(), shape.getOffset().hashCode(), workingDensity, shape.getVersion(), + computeModifierHash()); if (shape.isDynamic() || needsUpdate || !state.equals(lastState) || cachedPoints.isEmpty()) { - Set points = (ordering != null) ? new TreeSet<>(ordering) : new LinkedHashSet<>(); + List points = new ArrayList<>(); - shape.beforeSampling(density); + shape.beforeSampling(workingDensity); switch (style) { - case OUTLINE -> shape.generateOutline(points, density); - case SURFACE -> shape.generateSurface(points, density); - case FILL -> shape.generateFilled(points, density); + case OUTLINE -> shape.generateOutline(points, workingDensity); + case SURFACE -> shape.generateSurface(points, workingDensity); + case FILL -> shape.generateFilled(points, workingDensity); } shape.afterSampling(points); + // Apply geometry modifiers per-point + List> geoMods = new ArrayList<>(); + for (PointModifier mod : modifiers) { + if (mod instanceof PreRenderPointModifier) //noinspection unchecked + geoMods.add((PointModifier) mod); + } + if (!geoMods.isEmpty()) { + ShapeBounds bounds = ShapeBounds.compute(points); + PointContext ctx = new PointContext(); + ctx.shape = shape; + ctx.bounds = bounds; + ctx.totalPoints = points.size(); + for (PointModifier mod : geoMods) { + mod.prepare(bounds); + } + for (int i = 0; i < points.size(); i++) { + Vector3d p = points.get(i); + ctx.x = p.x; ctx.y = p.y; ctx.z = p.z; + ctx.index = i; + for (PointModifier mod : geoMods) { + mod.modify(ctx); + } + p.x = ctx.x; p.y = ctx.y; p.z = ctx.z; + } + } + for (Vector3d point : points) { orientation.transform(point); point.mul(shape.getScale()); point.add(shape.getOffset()); } + + if (ordering != null) { + points.sort(ordering); + } + cachedPoints = points; lastState = state; needsUpdate = false; @@ -63,55 +135,274 @@ public Set getPoints(Shape shape, Quaterniond orientation) { return cachedPoints; } + /** + * Drives the render loop for a shape. Gets cached points, prepares render modifiers, + * then calls the renderer for each point. + * + * @param shape the shape to render + * @param orientation the orientation to use for point sampling + * @param renderer the client-provided renderer + */ + @Override + public void render(Shape shape, Quaterniond orientation, ShapeRenderer renderer) { + List points = getPoints(shape, orientation); + Context context = renderer.getContext(); + + // Collect render modifiers (everything that is not a pre-render/geometry modifier) + List> renderMods = new ArrayList<>(); + Class ctxClass = context.getClass(); + for (PointModifier mod : modifiers) { + if (mod instanceof PreRenderPointModifier) continue; + if (!mod.contextType().isAssignableFrom(ctxClass)) { + throw new IllegalStateException( + "Render modifier " + mod.getClass().getSimpleName() + + " requires context type " + mod.contextType().getSimpleName() + + " but renderer provides " + ctxClass.getSimpleName() + ); + } + //noinspection unchecked + renderMods.add((PointModifier) mod); + } + + // Prepare render modifiers + ShapeBounds bounds = ShapeBounds.compute(points); + context.bounds = bounds; + context.totalPoints = points.size(); + context.shape = shape; + for (PointModifier mod : renderMods) { + mod.prepare(bounds); + } + + // Render loop + renderer.begin(points.size()); + for (int i = 0; i < points.size(); i++) { + Vector3d p = points.get(i); + context.reset(); + context.x = p.x; context.y = p.y; context.z = p.z; + context.index = i; + for (PointModifier mod : renderMods) { + mod.modify(context); + } + renderer.renderPoint(context); + } + renderer.end(); + } + + /** + * Forces the point cache to be regenerated on the next call to + * {@link #getPoints(Shape)} or {@link #getPoints(Shape, Quaterniond)}. + * Useful when an external change affects the shape that is not tracked by the cache key. + */ public void markDirty() { needsUpdate = true; } + /** + * @return the current sampling style (outline, surface, or fill) + */ @Override - public SamplingStyle getStyle() { return style; } + public SamplingStyle getStyle() { + return style; + } + /** + * Sets the sampling style and invalidates the point cache. + * + * @param style the new sampling style + */ @Override public void setStyle(SamplingStyle style) { this.style = style; this.needsUpdate = true; } + /** + * Returns the current point spacing density. + * Only meaningful when density has been set explicitly via {@link #setDensity}; + * otherwise the value is overridden by the auto-computed density from {@link #setMaxPoints}. + * + * @return the current density value + */ @Override - public double getDensity() { return density; } + public double getDensity() { + return density; + } + /** + * Sets the point spacing density explicitly, overriding any max-points-based auto-density. + * Clamps the value to at least {@code Shape.EPSILON}. Invalidates the point cache. + * + * @param density the desired point spacing; smaller values produce more points + */ @Override public void setDensity(double density) { this.density = Math.max(density, Shape.EPSILON); + this.densityExplicit = true; + this.needsUpdate = true; + } + + /** + * @return the maximum number of points that auto-density calculation will target + */ + @Override + public int getMaxPoints() { + return maxPoints; + } + + /** + * Sets the target maximum number of points and switches to auto-density mode, + * where density is derived from the shape geometry to approximate this count. + * Clamps the value to at least {@code 1}. Invalidates the point cache. + * + * @param maxPoints the target maximum particle count + */ + @Override + public void setMaxPoints(int maxPoints) { + this.maxPoints = Math.max(1, maxPoints); + this.densityExplicit = false; this.needsUpdate = true; } + /** + * Returns whether density was set explicitly via {@link #setDensity}. + * When {@code false}, density is auto-computed from the shape to target {@link #getMaxPoints()}. + * + * @return {@code true} if density was set explicitly + */ @Override - public Comparator getOrdering() { return ordering; } + public boolean isDensityExplicit() { + return densityExplicit; + } + + /** + * @return the comparator used to sort sampled points, or {@code null} for insertion order + */ + @Override + public Comparator getOrdering() { + return ordering; + } + /** + * Sets the comparator used to sort sampled points after generation. + * Pass {@code null} to disable sorting. Invalidates the point cache. + * + * @param ordering a comparator over {@link org.joml.Vector3d}, or {@code null} + */ @Override public void setOrdering(Comparator ordering) { this.ordering = ordering; this.needsUpdate = true; } + /** + * Returns the unique identifier for this sampler instance. + * The UUID is generated at construction time and never changes. + * + * @return the immutable UUID of this sampler + */ @Override - public UUID getUUID() { return uuid; } + public UUID getUUID() { + return uuid; + } + /** + * @return the client-provided {@link DrawContext}, or {@code null} if none is set + */ @Override - public DrawContext getDrawContext() { return drawContext; } + public DrawContext getDrawContext() { + return drawContext; + } + /** + * Sets the client-provided rendering context. + * + * @param context the {@link DrawContext} to associate with this sampler + */ @Override public void setDrawContext(DrawContext context) { this.drawContext = context; } + /** + * Returns the live modifier list. Callers should prefer {@link #addModifier}, + * {@link #removeModifier}, and {@link #clearModifiers} to ensure cache invalidation. + * + * @return the mutable list of registered modifiers + */ + @Override + public List> getModifiers() { + return modifiers; + } + + /** + * Appends a modifier to the end of the modifier list. + * Geometry modifiers ({@link PointModifier.PreRenderPointModifier}) additionally + * invalidate the point cache. + * + * @param modifier the modifier to add + */ + @Override + public void addModifier(PointModifier modifier) { + modifiers.add(modifier); + if (modifier instanceof PreRenderPointModifier) needsUpdate = true; + } + + /** + * Removes a modifier from the list if present. + * If the removed modifier is a geometry modifier, the point cache is invalidated. + * + * @param modifier the modifier to remove + */ + @Override + public void removeModifier(PointModifier modifier) { + if (modifiers.remove(modifier)) { + if (modifier instanceof PreRenderPointModifier) needsUpdate = true; + } + } + + /** + * Removes all modifiers. If any geometry modifier was registered, + * the point cache is invalidated. + */ + @Override + public void clearModifiers() { + if (!modifiers.isEmpty()) { + // If any geometry modifier was present, invalidate cache + boolean hadGeo = modifiers.stream().anyMatch(m -> m instanceof PreRenderPointModifier); + modifiers.clear(); + if (hadGeo) needsUpdate = true; + } + } + + private int computeModifierHash() { + int hash = 1; + for (PointModifier mod : modifiers) { + if (mod instanceof PreRenderPointModifier) hash = 31 * hash + mod.modifierHash(); + } + return hash; + } + + /** + * Returns a deep copy of this sampler. The clone has an empty point cache + * (forcing a fresh sample on first use) but otherwise copies all configuration, + * including a deep-copied modifier list and a copied {@link DrawContext}. + * The UUID is not shared; the clone inherits the same UUID value. + * + * @return a new {@code DefaultPointSampler} with the same configuration + */ @Override public DefaultPointSampler clone() { try { DefaultPointSampler copy = (DefaultPointSampler) super.clone(); // Don't share the cache - copy.cachedPoints = new LinkedHashSet<>(); + copy.cachedPoints = new ArrayList<>(); copy.needsUpdate = true; + // maxPoints and densityExplicit are primitives — copied by super.clone() if (drawContext != null) copy.drawContext = drawContext.copy(); + // Deep-copy modifiers + copy.modifiers = new ArrayList<>(modifiers.size()); + for (PointModifier mod : modifiers) { + copy.modifiers.add(mod.clone()); + } return copy; } catch (CloneNotSupportedException e) { throw new AssertionError(e); @@ -119,5 +410,6 @@ public DefaultPointSampler clone() { } private record CacheState(SamplingStyle style, int orientationHash, double scale, - int offsetHash, double density, long shapeVersion) {} + int offsetHash, double density, long shapeVersion, + int modifierHash) {} } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/DrawContext.java b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/DrawContext.java index 19427f0..e20cda8 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/DrawContext.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/DrawContext.java @@ -4,5 +4,12 @@ * Client-provided rendering metadata. Implementations are opaque to the library. */ public interface DrawContext { + /** + * Returns an independent copy of this context. + * Used by {@link DefaultPointSampler#clone()} to ensure the cloned sampler + * does not share rendering state with the original. + * + * @return a new {@code DrawContext} with the same configuration + */ DrawContext copy(); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/PointSampler.java b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/PointSampler.java index 1d7221c..69b76e0 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/PointSampler.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/PointSampler.java @@ -1,11 +1,13 @@ package com.sovdee.shapes.sampling; +import com.sovdee.shapes.modifiers.PointContext; +import com.sovdee.shapes.modifiers.PointModifier; import com.sovdee.shapes.shapes.Shape; import org.joml.Quaterniond; import org.joml.Vector3d; import java.util.Comparator; -import java.util.Set; +import java.util.List; import java.util.UUID; /** @@ -14,29 +16,95 @@ */ public interface PointSampler extends Cloneable { + /** + * @return the current {@link SamplingStyle} (outline, surface, or fill) + */ SamplingStyle getStyle(); + + /** + * Sets the style used when sampling the shape's geometry. + * + * @param style the new sampling style + */ void setStyle(SamplingStyle style); + /** + * Returns the explicit point spacing density, if one has been set. + * When density is not explicit, implementations derive it from {@link #getMaxPoints()}. + * + * @return the current density value + */ double getDensity(); + + /** + * Sets an explicit point spacing density. + * Smaller values produce more points; calling this switches the sampler to explicit-density mode. + * + * @param density the desired point spacing + */ void setDensity(double density); + /** + * @return the target maximum number of points used in auto-density mode + */ + int getMaxPoints(); + + /** + * Sets the target maximum number of points and enables auto-density mode, + * where the implementation derives an appropriate density from the shape geometry. + * + * @param maxPoints the target maximum particle count + */ + void setMaxPoints(int maxPoints); + + /** + * Returns whether density was set explicitly via {@link #setDensity}. + * When {@code false}, density is auto-computed from the shape to target {@link #getMaxPoints()}. + * + * @return {@code true} if density is explicit + */ + boolean isDensityExplicit(); + + /** + * @return the comparator used to sort sampled points, or {@code null} for insertion order + */ Comparator getOrdering(); + + /** + * Sets the comparator used to sort sampled points after generation. + * + * @param ordering a comparator over {@link Vector3d}, or {@code null} to disable sorting + */ void setOrdering(Comparator ordering); + /** + * Returns the unique identifier assigned to this sampler at construction time. + * + * @return the immutable UUID of this sampler + */ UUID getUUID(); + /** + * @return the client-provided {@link DrawContext}, or {@code null} if none is set + */ DrawContext getDrawContext(); + + /** + * Associates a client-provided rendering context with this sampler. + * + * @param context the {@link DrawContext} to use during rendering + */ void setDrawContext(DrawContext context); /** * Samples points from the given shape using the shape's own orientation. */ - Set getPoints(Shape shape); + List getPoints(Shape shape); /** * Samples points from the given shape using the given orientation. */ - Set getPoints(Shape shape, Quaterniond orientation); + List getPoints(Shape shape, Quaterniond orientation); /** * Computes and sets the density to achieve approximately the given particle count. @@ -45,5 +113,52 @@ default void setParticleCount(Shape shape, int count) { setDensity(shape.computeDensity(getStyle(), count)); } + // ---- Modifier management ---- + + /** + * Returns the list of registered {@link PointModifier}s. + * Geometry modifiers are applied during point sampling; render modifiers are applied per frame. + * + * @return the mutable modifier list + */ + List> getModifiers(); + + /** + * Appends a modifier to the modifier list. + * Geometry modifiers may additionally invalidate the point cache. + * + * @param modifier the modifier to add + */ + void addModifier(PointModifier modifier); + + /** + * Removes a modifier from the list if present. + * Geometry modifiers may additionally invalidate the point cache. + * + * @param modifier the modifier to remove + */ + void removeModifier(PointModifier modifier); + + /** + * Removes all modifiers from the list. + * Geometry modifiers may additionally invalidate the point cache. + */ + void clearModifiers(); + + /** + * Drives the full render loop: gets cached points, runs render modifiers, calls renderer. + * + * @param shape the shape to render + * @param orientation the orientation for point sampling + * @param renderer client-provided renderer + */ + void render(Shape shape, Quaterniond orientation, ShapeRenderer renderer); + + /** + * Returns a deep copy of this sampler with the same configuration. + * The clone must be fully independent from the original. + * + * @return a new sampler instance + */ PointSampler clone(); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/SamplingStyle.java b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/SamplingStyle.java index d65ef9b..ba198d4 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/SamplingStyle.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/SamplingStyle.java @@ -8,6 +8,11 @@ public enum SamplingStyle { SURFACE, FILL; + /** + * Returns a lowercase string representation of this style, e.g. {@code "outline"}. + * + * @return the lowercase enum name + */ @Override public String toString() { return name().toLowerCase(); diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/sampling/ShapeRenderer.java b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/ShapeRenderer.java new file mode 100644 index 0000000..aa0b9cf --- /dev/null +++ b/shapes-lib/src/main/java/com/sovdee/shapes/sampling/ShapeRenderer.java @@ -0,0 +1,38 @@ +package com.sovdee.shapes.sampling; + +import com.sovdee.shapes.modifiers.PointContext; + +/** + * Client-implemented interface for rendering shape points. + * The library drives the render loop; the client handles the actual output (e.g. NMS packets). + * + *

Implementations may cast the {@link PointContext} to their own subclass (e.g. {@code RenderContext}) + * to read render-specific properties written by render modifiers. + */ +public interface ShapeRenderer { + + /** + * Called once before the point loop. Use to allocate buffers, pre-compute shared state, etc. + * + * @param totalPoints the number of points that will be rendered + */ + void begin(int totalPoints); + + /** + * Called for each point after all render modifiers have been applied. + * Should be allocation-free. + * + * @param point the mutable per-point context; cast to your subclass to access render properties + */ + void renderPoint(Context point); + + /** + * @return Returns the context this renderer uses. + */ + Context getContext(); + + /** + * Called after all points have been rendered. Flush buffers, send remaining packets, etc. + */ + void end(); +} diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/AbstractShape.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/AbstractShape.java index 1ba1478..2d96188 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/AbstractShape.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/AbstractShape.java @@ -6,8 +6,8 @@ import org.joml.Quaterniond; import org.joml.Vector3d; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; /** * Base implementation of {@link Shape} providing spatial transform, versioning, @@ -22,6 +22,11 @@ public abstract class AbstractShape implements Shape { private long version = 0; private PointSampler pointSampler; + /** + * Initialises a new shape with an identity orientation, scale {@code 1.0}, zero offset, + * and a fresh {@link DefaultPointSampler}. Subclasses must call {@code super()} before + * setting dimension-specific fields. + */ public AbstractShape() { this.orientation = new Quaterniond(); this.scale = 1; @@ -31,29 +36,51 @@ public AbstractShape() { // --- Spatial transform --- + /** + * {@inheritDoc} + * Returns a defensive copy so callers cannot mutate the internal quaternion. + */ @Override public Quaterniond getOrientation() { return new Quaterniond(orientation); } + /** + * {@inheritDoc} + * The value is copied into the internal quaternion; the argument is not retained. + */ @Override public void setOrientation(Quaterniond orientation) { this.orientation.set(orientation); } + /** + * {@inheritDoc} + */ @Override public double getScale() { return scale; } + /** + * {@inheritDoc} + */ @Override public void setScale(double scale) { this.scale = scale; } + /** + * {@inheritDoc} + * Returns a defensive copy so callers cannot mutate the internal offset. + */ @Override public Vector3d getOffset() { return new Vector3d(offset); } + /** + * {@inheritDoc} + * The provided vector is stored directly (not copied); callers should not mutate it afterward. + */ @Override public void setOffset(Vector3d offset) { this.offset = offset; @@ -61,16 +88,28 @@ public void setOffset(Vector3d offset) { // --- Oriented axes --- + /** + * {@inheritDoc} + * Computed by rotating {@code (1, 0, 0)} with the internal orientation quaternion. + */ @Override public Vector3d getRelativeXAxis() { return orientation.transform(new Vector3d(1, 0, 0)); } + /** + * {@inheritDoc} + * Computed by rotating {@code (0, 1, 0)} with the internal orientation quaternion. + */ @Override public Vector3d getRelativeYAxis() { return orientation.transform(new Vector3d(0, 1, 0)); } + /** + * {@inheritDoc} + * Computed by rotating {@code (0, 0, 1)} with the internal orientation quaternion. + */ @Override public Vector3d getRelativeZAxis() { return orientation.transform(new Vector3d(0, 0, 1)); @@ -78,6 +117,10 @@ public Vector3d getRelativeZAxis() { // --- Change detection --- + /** + * {@inheritDoc} + * The counter starts at {@code 0} and is incremented each time {@link #invalidate()} is called. + */ @Override public long getVersion() { return version; } @@ -91,24 +134,55 @@ protected void invalidate() { // --- Dynamic support --- + /** + * {@inheritDoc} + */ @Override public boolean isDynamic() { return dynamic; } + /** + * {@inheritDoc} + */ @Override public void setDynamic(boolean dynamic) { this.dynamic = dynamic; } // --- Point generation defaults --- + /** + * Default surface implementation that delegates to {@link #generateOutline(List, double)}. + * Subclasses with a meaningful surface representation (e.g. {@link Circle}, {@link Sphere}) + * should override this method. + * + * @param points the list to append generated points to; must not be null + * @param density the desired spacing between points + */ @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { generateOutline(points, density); } + /** + * Default filled implementation that delegates to {@link #generateSurface(List, double)}. + * Subclasses that can meaningfully fill their volume (e.g. {@link Circle}, {@link Sphere}) + * should override this method. + * + * @param points the list to append generated points to; must not be null + * @param density the desired spacing between points + */ @Override - public void generateFilled(Set points, double density) { + public void generateFilled(List points, double density) { generateSurface(points, density); } + /** + * Generic fallback implementation that always returns a density of {@code 0.25}. + * Subclasses should override this with a formula appropriate to their geometry so that + * {@link PointSampler#setParticleCount(Shape, int)} produces accurate results. + * + * @param style the {@link SamplingStyle} being used (OUTLINE, SURFACE, or FILL) + * @param targetPointCount the desired number of points + * @return {@code 0.25} as a safe, conservative default + */ @Override public double computeDensity(SamplingStyle style, int targetPointCount) { // Generic fallback — subclasses should override for accuracy @@ -117,24 +191,52 @@ public double computeDensity(SamplingStyle style, int targetPointCount) { // --- Geometry query --- + /** + * {@inheritDoc} + * Concrete subclasses must implement the containment test appropriate to their geometry. + */ @Override public abstract boolean contains(Vector3d point); // --- PointSampler --- + /** + * {@inheritDoc} + */ @Override public PointSampler getPointSampler() { return pointSampler; } + /** + * {@inheritDoc} + */ @Override public void setPointSampler(PointSampler sampler) { this.pointSampler = sampler; } // --- Vertical fill helper --- - protected static void fillVertically(Set points, double height, double density) { - Set base = new LinkedHashSet<>(points); + /** + * Duplicates an existing slice of {@code points} at successive Y offsets to fill a vertical + * column of the given {@code height}. The Y step is derived from {@code density} so that + * layer spacing is as uniform as possible. + *

+ * The method reads all points in the range {@code [startIndex, points.size())} at the time + * of the call and appends copies translated by {@code y = density, 2*density, ...} up to + * (but not including) {@code height}. + * + * @param points the list of points to extend; the existing slice is treated as the base layer + * @param startIndex the index of the first point in the base layer + * @param height the total vertical extent to fill; must be positive + * @param density the desired spacing between vertical layers + */ + protected static void fillVertically(List points, int startIndex, double height, double density) { + int baseSize = points.size() - startIndex; double heightStep = height / Math.round(height / density); - for (double y = 0; y < height; y += heightStep) { - for (Vector3d v : base) { + if (points instanceof ArrayList pointList) { + pointList.ensureCapacity(points.size() + baseSize * (int) ((height - heightStep) / heightStep + 1)); + } + for (double y = heightStep; y < height; y += heightStep) { + for (int i = startIndex; i < startIndex + baseSize; i++) { + Vector3d v = points.get(i); points.add(new Vector3d(v.x, y, v.z)); } } @@ -142,9 +244,22 @@ protected static void fillVertically(Set points, double height, double // --- Replication --- + /** + * {@inheritDoc} + * Concrete subclasses must create a new instance with the same dimension-specific state and + * then call {@link #copyTo(Shape)} to propagate the base transform fields. + */ @Override public abstract Shape clone(); + /** + * {@inheritDoc} + * Copies orientation, scale, offset, dynamic flag, and a clone of the point sampler into + * {@code shape}. Dimension-specific fields are not touched. + * + * @param shape the target shape; must not be null + * @return {@code shape}, for chaining convenience + */ @Override public Shape copyTo(Shape shape) { shape.setOrientation(new Quaterniond(this.orientation)); @@ -152,14 +267,7 @@ public Shape copyTo(Shape shape) { shape.setOffset(new Vector3d(this.offset)); shape.setDynamic(this.dynamic); - // Clone sampler config - PointSampler srcSampler = this.pointSampler; - PointSampler destSampler = shape.getPointSampler(); - destSampler.setStyle(srcSampler.getStyle()); - destSampler.setDensity(srcSampler.getDensity()); - destSampler.setOrdering(srcSampler.getOrdering()); - if (srcSampler.getDrawContext() != null) - destSampler.setDrawContext(srcSampler.getDrawContext().copy()); + shape.setPointSampler(this.pointSampler.clone()); return shape; } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Arc.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Arc.java index a4a8d7e..95f3efa 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Arc.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Arc.java @@ -2,36 +2,87 @@ import org.joml.Vector3d; -import java.util.Set; +import java.util.List; +/** + * An arc is a partial {@link Circle}: a sector of a circle swept from angle {@code 0} to + * {@code cutoffAngle} radians around the Y axis. When a non-zero height is provided it becomes + * a partial cylinder (a cylindrical wedge). + *

+ * The cutoff angle is clamped to {@code [0, 2π]}. A cutoff of {@code 2π} produces a full + * circle or cylinder, identical in geometry to the parent {@link Circle}. Angles are measured + * in the XZ plane starting from the positive X axis. + *

+ * Surface generation delegates to {@link #generateFilled} so that sector fills are consistent + * across all three {@link com.sovdee.shapes.sampling.SamplingStyle} modes. + */ public class Arc extends Circle implements CutoffShape { + /** + * Creates a flat (zero-height) arc with the given radius and angular span. + * + * @param radius the radius of the arc; clamped to at least {@link Shape#EPSILON} + * @param cutoffAngle the angular span of the arc in radians; clamped to {@code [0, 2π]} + */ public Arc(double radius, double cutoffAngle) { super(radius); this.cutoffAngle = Math.clamp(cutoffAngle, 0, Math.PI * 2); } + /** + * Creates a cylindrical-wedge arc with the given radius, height, and angular span. + * + * @param radius the radius of the arc; clamped to at least {@link Shape#EPSILON} + * @param height the vertical extent of the arc; clamped to {@code >= 0} + * @param cutoffAngle the angular span of the arc in radians; clamped to {@code [0, 2π]} + */ public Arc(double radius, double height, double cutoffAngle) { super(radius, height); this.cutoffAngle = Math.clamp(cutoffAngle, 0, Math.PI * 2); } + /** + * Generates surface points by delegating to {@link #generateFilled(List, double)} so that + * the surface of an arc sector is treated as a filled disc segment rather than just an outline. + * + * @param points the list to append generated points to; must not be null + * @param density the desired spacing between points + */ @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { generateFilled(points, density); } + /** + * {@inheritDoc} + * + * @return the cutoff angle in radians, in the range {@code [0, 2π]} + */ @Override public double getCutoffAngle() { return this.cutoffAngle; } + /** + * {@inheritDoc} + * Clamps the given angle to {@code [0, 2π]} and invalidates the point cache. + * + * @param cutoffAngle the new cutoff angle in radians; clamped to {@code [0, 2π]} + */ @Override public void setCutoffAngle(double cutoffAngle) { this.cutoffAngle = Math.clamp(cutoffAngle, 0, Math.PI * 2); invalidate(); } + /** + * Returns {@code true} if {@code point} is within the parent {@link Circle}'s disc/cylinder + * and its XZ angle is within the cutoff span. The angle is measured from the positive X axis + * and normalised to {@code [0, 2π]} using {@code atan2(z, x)}. + * + * @param point the point to test, in local coordinates + * @return {@code true} if the point lies inside this arc sector + */ @Override public boolean contains(Vector3d point) { if (!super.contains(point)) return false; @@ -40,11 +91,22 @@ public boolean contains(Vector3d point) { return angle <= cutoffAngle; } + /** + * Returns a deep copy of this arc with identical radius, height, cutoff angle, and base + * transform state. + * + * @return a new {@link Arc} equal to this one + */ @Override public Shape clone() { return this.copyTo(new Arc(this.getRadius(), this.getHeight(), cutoffAngle)); } + /** + * Returns a human-readable description of this arc, including its radius, cutoff angle, and height. + * + * @return a string of the form {@code Arc{radius=..., cutoffAngle=..., height=...}} + */ @Override public String toString() { return "Arc{radius=" + this.getRadius() + ", cutoffAngle=" + cutoffAngle + ", height=" + this.getHeight() + '}'; diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/BezierCurve.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/BezierCurve.java index 734f01a..15b34ec 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/BezierCurve.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/BezierCurve.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.function.Supplier; /** @@ -17,41 +16,70 @@ public class BezierCurve extends AbstractShape { private List controlPoints; private Supplier> controlPointsSupplier; + /** + * Creates a static Bezier curve from a fixed list of control points. + * The points are deep-copied so subsequent mutation of the list has no effect. + * + * @param controlPoints the control points defining the curve; must contain at least 2 elements + * @throws IllegalArgumentException if fewer than 2 control points are provided + */ public BezierCurve(List controlPoints) { super(); if (controlPoints.size() < 2) throw new IllegalArgumentException("A bezier curve must have at least 2 control points."); - this.controlPoints = new ArrayList<>(); + this.controlPoints = new ArrayList<>(controlPoints.size()); for (Vector3d cp : controlPoints) this.controlPoints.add(new Vector3d(cp)); } + /** + * Creates a dynamic Bezier curve whose control points are re-fetched from {@code controlPointsSupplier} + * on every render call. The shape is automatically marked {@link #setDynamic(boolean) dynamic}. + * The supplier is invoked once during construction to validate the initial point count. + * + * @param controlPointsSupplier a supplier that returns the live control points; must return + * at least 2 points on every invocation + * @throws IllegalArgumentException if the initial supplier result contains fewer than 2 points + */ public BezierCurve(Supplier> controlPointsSupplier) { super(); this.controlPointsSupplier = controlPointsSupplier; List pts = controlPointsSupplier.get(); if (pts.size() < 2) throw new IllegalArgumentException("A bezier curve must have at least 2 control points."); - this.controlPoints = new ArrayList<>(); - for (Vector3d cp : pts) - this.controlPoints.add(new Vector3d(cp)); + this.controlPoints = new ArrayList<>(pts); setDynamic(true); } + /** + * Copy constructor: creates a static Bezier curve with deep-copied control points from + * {@code curve}. The supplier is not copied; the result is always a static curve. + * + * @param curve the source curve to copy control points from; must not be null + */ public BezierCurve(BezierCurve curve) { super(); - this.controlPoints = new ArrayList<>(); + this.controlPoints = new ArrayList<>(curve.controlPoints.size()); for (Vector3d cp : curve.controlPoints) this.controlPoints.add(new Vector3d(cp)); } + /** + * Generates points along the Bezier curve using the de Casteljau algorithm and appends them + * to {@code points}. If a control-points supplier is set it is invoked now to refresh the + * control point list before sampling. + *

+ * The number of steps is {@code estimateLength() / density}, so smaller {@code density} values + * produce a finer approximation of the smooth curve. + * + * @param points the list to append generated points to; must not be null + * @param density the desired arc-length spacing between consecutive curve samples + */ @Override - public void generateOutline(Set points, double density) { + public void generateOutline(List points, double density) { if (controlPointsSupplier != null) { List pts = controlPointsSupplier.get(); - this.controlPoints = new ArrayList<>(); - for (Vector3d cp : pts) - this.controlPoints.add(new Vector3d(cp)); + this.controlPoints = new ArrayList<>(pts); } int steps = (int) (estimateLength() / density); int n = controlPoints.size(); @@ -82,12 +110,30 @@ private double estimateLength() { return dist; } + /** + * Computes the density required to produce approximately {@code targetPointCount} points along + * this curve. The result is {@code estimateLength() / targetPointCount}, where + * {@code estimateLength()} is the sum of straight-line distances between consecutive control points. + * + * @param style ignored; the Bezier curve has only one meaningful sampling mode + * @param targetPointCount the desired number of curve samples; clamped to at least 1 + * @return the density value to pass to {@link #generateOutline(List, double)} + */ @Override public double computeDensity(SamplingStyle style, int targetPointCount) { int count = Math.max(targetPointCount, 1); return estimateLength() / count; } + /** + * Returns {@code true} if {@code point} lies approximately on the curve. + * The test uses a high-density sampling of the curve (approximately one sample per {@code 0.1} + * units of chord length) and checks whether any sampled point is within {@link Shape#EPSILON} + * of {@code point}. + * + * @param point the point to test, in local coordinates + * @return {@code true} if the point is within {@link Shape#EPSILON} of the curve + */ @Override public boolean contains(Vector3d point) { // Approximate: check distance to nearest sampled point @@ -110,21 +156,46 @@ public boolean contains(Vector3d point) { return false; } + /** + * Returns the current list of control points. For dynamic curves this reflects the most + * recently fetched values from the supplier (i.e. after the last {@link #generateOutline} call). + * + * @return the live control point list; modifications affect the internal state directly + */ public List getControlPoints() { return controlPoints; } + /** + * Replaces the control points of this curve with a deep copy of {@code controlPoints} and + * invalidates the point cache. The supplier is not updated. + * + * @param controlPoints the new control points; must not be null or empty + */ public void setControlPoints(List controlPoints) { - this.controlPoints = new ArrayList<>(); + this.controlPoints = new ArrayList<>(controlPoints.size()); for (Vector3d cp : controlPoints) this.controlPoints.add(new Vector3d(cp)); invalidate(); } + /** + * Returns the supplier used to refresh control points on each render call, or {@code null} + * if this is a static curve constructed from a fixed point list. + * + * @return the control-points supplier, or {@code null} for static curves + */ public Supplier> getControlPointsSupplier() { return controlPointsSupplier; } + /** + * Returns a deep copy of this curve. If a control-points supplier is present the clone shares + * the same supplier reference (suppliers are not themselves cloneable). Otherwise, control + * points are deep-copied. Base transform state is copied via {@link #copyTo(Shape)}. + * + * @return a new {@link BezierCurve} with the same geometry and transform state + */ @Override public Shape clone() { BezierCurve clone; diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Circle.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Circle.java index 5e7b510..d7b5d32 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Circle.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Circle.java @@ -3,19 +3,55 @@ import com.sovdee.shapes.sampling.SamplingStyle; import org.joml.Vector3d; -import java.util.LinkedHashSet; -import java.util.Set; - +import java.util.ArrayList; +import java.util.List; + +/** + * A circle (or cylinder when {@code height > 0}) centred at the local origin in the XZ plane. + *

+ * With {@code height == 0} the three generation modes produce: + *

    + *
  • {@link #generateOutline} — a ring of points at the given radius
  • + *
  • {@link #generateSurface} — a filled disc (solid circle)
  • + *
  • {@link #generateFilled} — same as surface for a flat shape
  • + *
+ * With {@code height > 0} the modes produce: + *
    + *
  • {@link #generateOutline} — two rings at Y = 0 and Y = height, connected by vertical lines
  • + *
  • {@link #generateSurface} — a full cylinder (two discs + wall)
  • + *
  • {@link #generateFilled} — a solid cylindrical volume
  • + *
+ *

+ * The protected field {@code cutoffAngle} is inherited by {@link Arc} to limit the angular span. + * In a plain {@code Circle} this is always {@code 2π}. All angles are measured from the positive + * X axis in the XZ plane. + *

+ * {@code Circle} implements both {@link RadialShape} and {@link LWHShape}; the {@code length} and + * {@code width} dimensions are not meaningful for a circle and return/accept {@code 0}. Only + * {@code height} is active via {@link LWHShape}. + */ public class Circle extends AbstractShape implements RadialShape, LWHShape { private double radius; protected double cutoffAngle; private double height; + /** + * Creates a flat circle (height = 0) with the given radius. + * + * @param radius the radius of the circle; clamped to at least {@link Shape#EPSILON} + */ public Circle(double radius) { this(radius, 0); } + /** + * Creates a circle or cylinder with the given radius and height. + * A height of {@code 0} produces a flat 2-D circle. + * + * @param radius the radius; clamped to at least {@link Shape#EPSILON} + * @param height the vertical extent; clamped to {@code >= 0} + */ public Circle(double radius, double height) { super(); this.radius = Math.max(radius, Shape.EPSILON); @@ -25,68 +61,99 @@ public Circle(double radius, double height) { // --- Static calculation methods --- - public static Set calculateCircle(double radius, double density, double cutoffAngle) { - Set points = new LinkedHashSet<>(); + /** + * Appends points evenly spaced along a circular arc of the given {@code radius} in the XZ + * plane. The angular step is {@code density / radius} radians, so arc-length spacing between + * consecutive points equals approximately {@code density}. + * + * @param points the list to append to; must not be null + * @param radius the radius of the circle + * @param density the desired arc-length spacing between points + * @param cutoffAngle the angular span to cover, in radians ({@code [0, 2π]}) + */ + public static void calculateCircle(List points, double radius, double density, double cutoffAngle) { double stepSize = density / radius; + if (points instanceof ArrayList al) + al.ensureCapacity(points.size() + (int) (cutoffAngle / stepSize) + 1); for (double theta = 0; theta < cutoffAngle; theta += stepSize) { points.add(new Vector3d(Math.cos(theta) * radius, 0, Math.sin(theta) * radius)); } - return points; } - public static Set calculateDisc(double radius, double density, double cutoffAngle) { - Set points = new LinkedHashSet<>(); + /** + * Appends points filling the area of a disc sector in the XZ plane by stacking concentric + * rings from {@code density} to {@code radius} (inclusive) at intervals of {@code density}. + * Each ring is generated by {@link #calculateCircle}. + * + * @param points the list to append to; must not be null + * @param radius the outer radius of the disc + * @param density the desired spacing between points and between concentric rings + * @param cutoffAngle the angular span of the sector, in radians ({@code [0, 2π]}) + */ + public static void calculateDisc(List points, double radius, double density, double cutoffAngle) { + if (points instanceof ArrayList al) + al.ensureCapacity(points.size() + (int) (cutoffAngle * radius * radius / (density * density * Math.PI))); for (double subRadius = density; subRadius < radius; subRadius += density) { - points.addAll(calculateCircle(subRadius, density, cutoffAngle)); + calculateCircle(points, subRadius, density, cutoffAngle); } - points.addAll(calculateCircle(radius, density, cutoffAngle)); - return points; + calculateCircle(points, radius, density, cutoffAngle); } - public static Set calculateCylinder(double radius, double height, double density, double cutoffAngle) { - Set points = calculateDisc(radius, density, cutoffAngle); - // Top disc via direct loop - Set top = new LinkedHashSet<>(); - for (Vector3d v : points) { - top.add(new Vector3d(v.x, height, v.z)); + /** + * Appends points filling a cylindrical (or wedge-cylinder) volume. The method generates: + *

    + *
  1. A bottom disc at Y = 0.
  2. + *
  3. A top disc at Y = {@code height} (copied from the bottom disc).
  4. + *
  5. The curved wall, filled vertically via {@link #fillVertically}.
  6. + *
+ * + * @param points the list to append to; must not be null + * @param radius the radius of the cylinder + * @param height the height of the cylinder; must be positive + * @param density the desired spacing between points + * @param cutoffAngle the angular span of the cylinder sector, in radians ({@code [0, 2π]}) + */ + public static void calculateCylinder(List points, double radius, double height, double density, double cutoffAngle) { + // Bottom disc + int discStart = points.size(); + calculateDisc(points, radius, density, cutoffAngle); + int discEnd = points.size(); + // Top disc - copy bottom disc at height + for (int i = discStart; i < discEnd; i++) { + Vector3d v = points.get(i); + points.add(new Vector3d(v.x, height, v.z)); } - points.addAll(top); // Wall - Set wall = calculateCircle(radius, density, cutoffAngle); - fillVertically(wall, height, density); - points.addAll(wall); - return points; + int wallStart = points.size(); + calculateCircle(points, radius, density, cutoffAngle); + fillVertically(points, wallStart, height, density); } // --- Generation methods --- @Override - public void generateOutline(Set points, double density) { - Set circle = calculateCircle(radius, density, cutoffAngle); + public void generateOutline(List points, double density) { + int start = points.size(); + calculateCircle(points, radius, density, cutoffAngle); if (height != 0) { - fillVertically(circle, height, density); - points.addAll(circle); - } else { - points.addAll(circle); + fillVertically(points, start, height, density); } } @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { if (height != 0) - points.addAll(calculateCylinder(radius, height, density, cutoffAngle)); + calculateCylinder(points, radius, height, density, cutoffAngle); else - points.addAll(calculateDisc(radius, density, cutoffAngle)); + calculateDisc(points, radius, density, cutoffAngle); } @Override - public void generateFilled(Set points, double density) { - Set disc = calculateDisc(radius, density, cutoffAngle); + public void generateFilled(List points, double density) { + int start = points.size(); + calculateDisc(points, radius, density, cutoffAngle); if (height != 0) { - fillVertically(disc, height, density); - points.addAll(disc); - } else { - points.addAll(disc); + fillVertically(points, start, height, density); } } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Cuboid.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Cuboid.java index 57b1e0c..9ddedef 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Cuboid.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Cuboid.java @@ -3,12 +3,20 @@ import com.sovdee.shapes.sampling.SamplingStyle; import org.joml.Vector3d; -import java.util.Set; +import java.util.List; import java.util.function.Supplier; /** - * A cuboid shape, defined either by dimensions or by two corner vectors. - * For dynamic (entity-following) cuboids, use the plugin-side DynamicCuboid wrapper. + * A cuboid (rectangular prism) shape, defined either by explicit length/width/height dimensions + * or by two opposing corner {@link Vector3d} positions. The cuboid is always axis-aligned in + * local space and centred at the origin, offset by any {@code centerOffset} computed from the + * corner positions. + *
+ * When constructed from two {@link java.util.function.Supplier Supplier<Vector3d>} corners the + * cuboid is marked as {@link AbstractShape#setDynamic(boolean) dynamic}, meaning the corner + * positions are re-evaluated each time {@link #beforeSampling(double)} is called. + *
+ * For entity-following dynamic cuboids in the plugin layer, use the {@code DynamicCuboid} wrapper. */ public class Cuboid extends AbstractShape implements LWHShape { @@ -18,6 +26,15 @@ public class Cuboid extends AbstractShape implements LWHShape { private Supplier cornerASupplier; private Supplier cornerBSupplier; + /** + * Constructs a cuboid centred at the local origin with the given dimensions. + * Each half-dimension is clamped to at least {@link Shape#EPSILON} to prevent + * degenerate geometry. + * + * @param length extent along the X axis (total, not half) + * @param width extent along the Z axis (total, not half) + * @param height extent along the Y axis (total, not half) + */ public Cuboid(double length, double width, double height) { super(); this.halfWidth = Math.max(width / 2, Shape.EPSILON); @@ -25,6 +42,16 @@ public Cuboid(double length, double width, double height) { this.halfHeight = Math.max(height / 2, Shape.EPSILON); } + /** + * Constructs a cuboid from two opposing corner positions expressed as world-space + * {@link Vector3d} values. The cuboid's dimensions are derived from the axis-aligned + * bounding box of the two corners, and the {@code centerOffset} is set to the midpoint + * so that drawing is centred correctly. + * + * @param cornerA first corner of the cuboid + * @param cornerB second corner, diagonally opposite to {@code cornerA} + * @throws IllegalArgumentException if both corners are equal + */ public Cuboid(Vector3d cornerA, Vector3d cornerB) { super(); if (cornerA.equals(cornerB)) @@ -35,6 +62,17 @@ public Cuboid(Vector3d cornerA, Vector3d cornerB) { centerOffset = new Vector3d(cornerB).add(cornerA).mul(0.5); } + /** + * Constructs a dynamic cuboid whose corners are provided by {@link Supplier} functions. + * The suppliers are called once during construction to compute initial dimensions, + * and again on every call to {@link #beforeSampling(double)} so that the cuboid tracks + * positions that change over time (e.g. entity locations). + *
+ * This constructor marks the shape as {@link AbstractShape#setDynamic(boolean) dynamic}. + * + * @param cornerA supplier returning the first corner position + * @param cornerB supplier returning the second corner position, diagonally opposite + */ public Cuboid(Supplier cornerA, Supplier cornerB) { super(); this.cornerASupplier = cornerA; @@ -53,6 +91,13 @@ private void calculateSteps(double density) { heightStep = 2 * halfHeight / Math.round(2 * halfHeight / density); } + /** + * Called before point sampling begins. If this cuboid was constructed with corner suppliers, + * re-evaluates those suppliers to update the half-dimensions to the current corner positions. + * Then pre-computes the per-axis step sizes used by the generation methods. + * + * @param density the target spacing between adjacent sample points + */ @Override public void beforeSampling(double density) { if (cornerASupplier != null && cornerBSupplier != null) { @@ -65,13 +110,30 @@ public void beforeSampling(double density) { calculateSteps(density); } + /** + * Called after all points have been generated. Translates every point by the {@code centerOffset} + * so that corner-defined cuboids are positioned correctly in world space rather than being + * centred at the local origin. + * + * @param points the mutable list of sampled points to translate in place + */ @Override - public void afterSampling(Set points) { + public void afterSampling(List points) { points.forEach(vector -> vector.add(centerOffset)); } + /** + * Generates the 12-edge wireframe of the cuboid. Points are placed along each of the + * four parallel edges in the X, Y, and Z directions using the pre-computed step sizes. + * Corner vertices are included; interior edge steps along non-primary directions avoid + * re-adding the already-placed corner points. + * + * @param points the list to which generated points are appended + * @param density the target spacing between adjacent points (pre-computed step sizes + * are used instead of {@code density} directly) + */ @Override - public void generateOutline(Set points, double density) { + public void generateOutline(List points, double density) { for (double x = -halfLength; x <= halfLength; x += lengthStep) { points.add(new Vector3d(x, -halfHeight, -halfWidth)); points.add(new Vector3d(x, -halfHeight, halfWidth)); @@ -92,8 +154,17 @@ public void generateOutline(Set points, double density) { } } + /** + * Generates points covering all six faces of the cuboid. The top and bottom (XZ) faces are + * sampled as full grids; the front/back (XY) and left/right (YZ) faces fill in the remaining + * interior rows/columns to avoid duplicating edge points already added by the adjacent faces. + * + * @param points the list to which generated points are appended + * @param density the target spacing between adjacent points (pre-computed step sizes + * are used instead of {@code density} directly) + */ @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { for (double x = -halfLength; x <= halfLength; x += lengthStep) { for (double z = -halfWidth; z <= halfWidth; z += widthStep) { points.add(new Vector3d(x, -halfHeight, z)); @@ -114,8 +185,17 @@ public void generateSurface(Set points, double density) { } } + /** + * Generates a uniform 3-D grid of points filling the interior of the cuboid. Every lattice + * site within {@code [-halfLength, halfLength] × [-halfHeight, halfHeight] × [-halfWidth, halfWidth]} + * is included, using the pre-computed axis step sizes. + * + * @param points the list to which generated points are appended + * @param density the target spacing between adjacent points (pre-computed step sizes + * are used instead of {@code density} directly) + */ @Override - public void generateFilled(Set points, double density) { + public void generateFilled(List points, double density) { for (double x = -halfLength; x <= halfLength; x += lengthStep) { for (double y = -halfHeight; y <= halfHeight; y += heightStep) { for (double z = -halfWidth; z <= halfWidth; z += widthStep) { @@ -125,6 +205,21 @@ public void generateFilled(Set points, double density) { } } + /** + * Estimates the point spacing required to produce approximately {@code targetPointCount} points + * for the given {@link SamplingStyle}. + *
    + *
  • OUTLINE: {@code density = totalEdgeLength / count}, where total edge length + * is {@code 8 * (L/2 + H/2 + W/2)}
  • + *
  • SURFACE: {@code density = sqrt(totalSurfaceArea / count)}, where surface area + * is {@code 2*(L*H + L*W + H*W)}
  • + *
  • FILL: {@code density = cbrt(volume / count)}, where volume is {@code L*W*H}
  • + *
+ * + * @param style the sampling style (OUTLINE, SURFACE, or FILL) + * @param targetPointCount the desired number of output points + * @return the estimated point spacing to achieve the target count + */ @Override public double computeDensity(SamplingStyle style, int targetPointCount) { int count = Math.max(1, targetPointCount); @@ -135,6 +230,14 @@ public double computeDensity(SamplingStyle style, int targetPointCount) { }; } + /** + * Returns {@code true} if the given point lies inside or on the surface of this cuboid. + * Tests are performed in local (centred) space: the point is considered contained when + * {@code |x| ≤ halfLength && |y| ≤ halfHeight && |z| ≤ halfWidth}. + * + * @param point the point to test, in local shape space + * @return {@code true} if {@code point} is within the axis-aligned bounds of the cuboid + */ @Override public boolean contains(Vector3d point) { return Math.abs(point.x) <= halfLength && @@ -142,36 +245,89 @@ public boolean contains(Vector3d point) { Math.abs(point.z) <= halfWidth; } + /** + * Returns the total length of the cuboid along the X axis ({@code halfLength * 2}). + * + * @return the full X-axis extent + */ @Override public double getLength() { return halfLength * 2; } + /** + * Sets the total length of the cuboid along the X axis. Values below {@link Shape#EPSILON} + * are clamped to {@code EPSILON} to prevent degenerate geometry. Invalidates the point cache. + * + * @param length the new X-axis extent (total, not half) + */ @Override public void setLength(double length) { this.halfLength = Math.max(length / 2, Shape.EPSILON); invalidate(); } + /** + * Returns the total width of the cuboid along the Z axis ({@code halfWidth * 2}). + * + * @return the full Z-axis extent + */ @Override public double getWidth() { return halfWidth * 2; } + /** + * Sets the total width of the cuboid along the Z axis. Values below {@link Shape#EPSILON} + * are clamped to {@code EPSILON}. Invalidates the point cache. + * + * @param width the new Z-axis extent (total, not half) + */ @Override public void setWidth(double width) { this.halfWidth = Math.max(width / 2, Shape.EPSILON); invalidate(); } + /** + * Returns the total height of the cuboid along the Y axis ({@code halfHeight * 2}). + * + * @return the full Y-axis extent + */ @Override public double getHeight() { return halfHeight * 2; } + /** + * Sets the total height of the cuboid along the Y axis. Values below {@link Shape#EPSILON} + * are clamped to {@code EPSILON}. Invalidates the point cache. + * + * @param height the new Y-axis extent (total, not half) + */ @Override public void setHeight(double height) { this.halfHeight = Math.max(height / 2, Shape.EPSILON); invalidate(); } + /** + * Returns the {@link Supplier} used to compute the first corner of this cuboid, or + * {@code null} if this cuboid was not constructed with suppliers. + * + * @return the corner-A position supplier, or {@code null} + */ public Supplier getCornerASupplier() { return cornerASupplier; } + + /** + * Returns the {@link Supplier} used to compute the second corner of this cuboid, or + * {@code null} if this cuboid was not constructed with suppliers. + * + * @return the corner-B position supplier, or {@code null} + */ public Supplier getCornerBSupplier() { return cornerBSupplier; } + /** + * Creates a deep copy of this cuboid, preserving all dimensions, the corner suppliers + * (if present), the {@code centerOffset}, and all inherited {@link AbstractShape} state + * copied via {@link AbstractShape#copyTo(AbstractShape)}. + * + * @return a new {@link Cuboid} identical to this one + */ @Override public Shape clone() { Cuboid cuboid; diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/CutoffShape.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/CutoffShape.java index 19d3760..ab773fd 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/CutoffShape.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/CutoffShape.java @@ -4,6 +4,21 @@ * Represents a shape that has a cutoff angle, like an arc. */ public interface CutoffShape extends Shape { + + /** + * Returns the angular cutoff that limits how much of the shape's circumference is generated. + * The value is in radians in the range {@code [0, 2π]}, where {@code 2π} produces the full shape + * and smaller values truncate it to a partial arc or wedge. + * + * @return the cutoff angle in radians + */ double getCutoffAngle(); + + /** + * Sets the angular cutoff for this shape. Values are clamped to {@code [0, 2π]} by implementations. + * A value of {@code 2π} restores the full shape; a value of {@code 0} produces no points. + * + * @param cutoffAngle the new cutoff angle in radians; clamped to {@code [0, 2π]} + */ void setCutoffAngle(double cutoffAngle); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Ellipse.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Ellipse.java index eb99dad..bf3ab4a 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Ellipse.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Ellipse.java @@ -4,10 +4,21 @@ import org.joml.Vector3d; import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; +/** + * An ellipse (or extruded elliptical cylinder) defined by two radii along the X and Z axes + * and an optional height along the Y axis. + *
+ * When {@code height} is zero the shape is a flat 2-D ellipse lying in the XZ plane. + * When {@code height} is positive the shape is extruded into a full cylinder-like solid + * whose cross-sections are ellipses. + *
+ * Point sampling is delegated to the public static helpers {@link #calculateEllipse}, + * {@link #calculateEllipticalDisc}, and {@link #calculateCylinder}, which are also reused by + * {@link Ellipsoid} and {@link EllipticalArc}. An arc cutoff is stored in {@link #cutoffAngle} + * (inherited by {@link EllipticalArc}) and defaults to {@code 2π} for a full ellipse. + */ public class Ellipse extends AbstractShape implements LWHShape { private double xRadius; @@ -15,10 +26,27 @@ public class Ellipse extends AbstractShape implements LWHShape { private double height; protected double cutoffAngle; + /** + * Constructs a flat 2-D ellipse with the given X- and Z-axis radii and zero height. + * Equivalent to {@code new Ellipse(xRadius, zRadius, 0)}. + * + * @param xRadius the semi-axis length along the X axis; clamped to at least {@link Shape#EPSILON} + * @param zRadius the semi-axis length along the Z axis; clamped to at least {@link Shape#EPSILON} + */ public Ellipse(double xRadius, double zRadius) { this(xRadius, zRadius, 0); } + /** + * Constructs a (possibly extruded) ellipse. When {@code height} is zero the result is a flat + * ellipse in the XZ plane. When {@code height} is positive the outline consists of a vertical + * ellipse ring extruded upward, the surface is a closed cylinder, and the fill is a solid + * elliptical prism. + * + * @param xRadius the semi-axis length along the X axis; clamped to at least {@link Shape#EPSILON} + * @param zRadius the semi-axis length along the Z axis; clamped to at least {@link Shape#EPSILON} + * @param height the extrusion distance along the Y axis; clamped to at least {@code 0} + */ public Ellipse(double xRadius, double zRadius, double height) { super(); this.xRadius = Math.max(xRadius, Shape.EPSILON); @@ -36,11 +64,26 @@ private static double ellipseCircumference(double r1, double r2) { return Math.PI * (a + b) * (1 + 3 * h / (10 + Math.sqrt(4 - 3 * h))); } - public static List calculateEllipse(double r1, double r2, double density, double cutoffAngle) { - List points = new ArrayList<>(); + /** + * Appends points along the perimeter of an ellipse with semi-axes {@code r1} (X) and + * {@code r2} (Z), using an arc-length-adaptive angle step so that consecutive points are + * spaced approximately {@code density} units apart. + *
+ * Sampling stops when the accumulated angle reaches {@code cutoffAngle}. Pass {@code 2π} + * for a complete ellipse. + * + * @param points the list to which points are appended + * @param r1 semi-axis length along the X axis + * @param r2 semi-axis length along the Z axis + * @param density desired arc-length spacing between consecutive points + * @param cutoffAngle maximum sweep angle in radians ({@code 0} to {@code 2π}) + */ + public static void calculateEllipse(List points, double r1, double r2, double density, double cutoffAngle) { double circumference = ellipseCircumference(r1, r2); int steps = (int) Math.round(circumference / density); + if (points instanceof ArrayList al) + al.ensureCapacity(points.size() + (int) (steps * cutoffAngle / (2 * Math.PI)) + 1); double theta = 0; double angleStep = 0; for (int i = 0; i < steps; i++) { @@ -53,67 +96,127 @@ public static List calculateEllipse(double r1, double r2, double densi angleStep = density / Math.sqrt(dx * dx + dy * dy); theta += angleStep; } - return points; } - public static Set calculateEllipticalDisc(double r1, double r2, double density, double cutoffAngle) { - Set points = new LinkedHashSet<>(); + /** + * Appends points filling the area of an elliptical disc (filled 2-D ellipse) by sampling + * concentric ellipses at uniform radial increments from the outermost radius down to zero. + * Each ring is generated by {@link #calculateEllipse} with proportionally scaled radii. + * + * @param points the list to which points are appended + * @param r1 outer semi-axis length along the X axis + * @param r2 outer semi-axis length along the Z axis + * @param density desired arc-length spacing used for both the ring step and {@link #calculateEllipse} + * @param cutoffAngle angular cutoff applied to each ring ({@code 2π} for a full disc) + */ + public static void calculateEllipticalDisc(List points, double r1, double r2, double density, double cutoffAngle) { + if (points instanceof ArrayList al) + al.ensureCapacity(points.size() + (int) (cutoffAngle * r1 * r2 / (density * density))); int steps = (int) Math.round(Math.max(r1, r2) / density); double r; for (double i = 1; i <= steps; i += 1) { r = i / steps; - points.addAll(calculateEllipse(r1 * r, r2 * r, density, cutoffAngle)); + calculateEllipse(points, r1 * r, r2 * r, density, cutoffAngle); } - return points; } - public static Set calculateCylinder(double r1, double r2, double height, double density, double cutoffAngle) { - Set points = calculateEllipticalDisc(r1, r2, density, cutoffAngle); - // Top disc via direct loop - Set top = new LinkedHashSet<>(); - for (Vector3d v : points) { - top.add(new Vector3d(v.x, height, v.z)); + /** + * Appends points covering a solid elliptical cylinder: bottom disc, top disc (offset by + * {@code height} along Y), and the lateral wall extruded vertically between them. + *
+ * The bottom and top discs are generated via {@link #calculateEllipticalDisc}; the wall is + * generated via {@link #calculateEllipse} and then extended vertically using the inherited + * {@code fillVertically} helper. + * + * @param points the list to which points are appended + * @param r1 semi-axis along the X axis + * @param r2 semi-axis along the Z axis + * @param height extrusion distance along the Y axis + * @param density desired spacing between adjacent points + * @param cutoffAngle angular cutoff applied to discs and the elliptical wall ({@code 2π} for full) + */ + public static void calculateCylinder(List points, double r1, double r2, double height, double density, double cutoffAngle) { + // Bottom disc + int discStart = points.size(); + calculateEllipticalDisc(points, r1, r2, density, cutoffAngle); + int discEnd = points.size(); + // Top disc - copy bottom disc at height + for (int i = discStart; i < discEnd; i++) { + Vector3d v = points.get(i); + points.add(new Vector3d(v.x, height, v.z)); } - points.addAll(top); // Wall - Set wall = new LinkedHashSet<>(calculateEllipse(r1, r2, density, cutoffAngle)); - fillVertically(wall, height, density); - points.addAll(wall); - return points; + int wallStart = points.size(); + calculateEllipse(points, r1, r2, density, cutoffAngle); + fillVertically(points, wallStart, height, density); } // --- Generation methods --- + /** + * Generates the outline of this ellipse. For a flat ellipse ({@code height == 0}) this is just + * the perimeter arc. When {@code height > 0} the outline ring is extruded vertically to form a + * rectangular strip connecting the bottom arc to the top arc. + * + * @param points the list to which generated points are appended + * @param density the desired spacing between adjacent points + */ @Override - public void generateOutline(Set points, double density) { - Set ellipse = new LinkedHashSet<>(calculateEllipse(xRadius, zRadius, density, cutoffAngle)); + public void generateOutline(List points, double density) { + int start = points.size(); + calculateEllipse(points, xRadius, zRadius, density, cutoffAngle); if (height != 0) { - fillVertically(ellipse, height, density); - points.addAll(ellipse); - } else { - points.addAll(ellipse); + fillVertically(points, start, height, density); } } + /** + * Generates the surface of this ellipse. When {@code height == 0} the surface is the filled + * elliptical disc in the XZ plane. When {@code height > 0} the surface consists of the top + * and bottom discs plus the lateral wall, computed by {@link #calculateCylinder}. + * + * @param points the list to which generated points are appended + * @param density the desired spacing between adjacent points + */ @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { if (height != 0) - points.addAll(calculateCylinder(xRadius, zRadius, height, density, cutoffAngle)); + calculateCylinder(points, xRadius, zRadius, height, density, cutoffAngle); else - points.addAll(calculateEllipticalDisc(xRadius, zRadius, density, cutoffAngle)); + calculateEllipticalDisc(points, xRadius, zRadius, density, cutoffAngle); } + /** + * Generates a solid fill of this ellipse. For a flat ellipse ({@code height == 0}) this is + * identical to the surface (the filled disc). When {@code height > 0} the bottom disc is + * generated then extended vertically by {@code fillVertically} to produce a solid elliptical prism. + * + * @param points the list to which generated points are appended + * @param density the desired spacing between adjacent points + */ @Override - public void generateFilled(Set points, double density) { - Set disc = calculateEllipticalDisc(xRadius, zRadius, density, cutoffAngle); + public void generateFilled(List points, double density) { + int start = points.size(); + calculateEllipticalDisc(points, xRadius, zRadius, density, cutoffAngle); if (height != 0) { - fillVertically(disc, height, density); - points.addAll(disc); - } else { - points.addAll(disc); + fillVertically(points, start, height, density); } } + /** + * Estimates the point spacing required to produce approximately {@code targetPointCount} points + * for the given {@link SamplingStyle}. + *
    + *
  • OUTLINE: uses Ramanujan's first approximation for the ellipse circumference divided + * by {@code count}
  • + *
  • SURFACE / FILL: {@code density = sqrt(π·xRadius·zRadius / count)}, the spacing + * that gives the target count over the disc area
  • + *
+ * + * @param style the sampling style (OUTLINE, SURFACE, or FILL) + * @param targetPointCount the desired number of output points + * @return the estimated point spacing to achieve the target count + */ @Override public double computeDensity(SamplingStyle style, int targetPointCount) { int count = Math.max(targetPointCount, 1); @@ -127,6 +230,15 @@ public double computeDensity(SamplingStyle style, int targetPointCount) { }; } + /** + * Returns {@code true} if the given point lies inside or on the boundary of this ellipse. + * The test first checks the normalised ellipse equation {@code (x/xRadius)² + (z/zRadius)² ≤ 1}. + * If the shape has height, the Y coordinate must additionally satisfy {@code 0 ≤ y ≤ height}; + * otherwise the point must lie in the XZ plane ({@code |y| < EPSILON}). + * + * @param point the point to test in local shape space + * @return {@code true} if the point is contained within the ellipse (or cylinder) + */ @Override public boolean contains(Vector3d point) { double nx = point.x / xRadius; @@ -136,33 +248,72 @@ public boolean contains(Vector3d point) { return Math.abs(point.y) < EPSILON; } + /** + * Returns the full diameter of the ellipse along the X axis ({@code xRadius * 2}). + * + * @return the total X-axis extent + */ @Override public double getLength() { return xRadius * 2; } + /** + * Sets the X-axis semi-axis from half of {@code length}. Values below {@link Shape#EPSILON} + * are clamped. Invalidates the point cache. + * + * @param length the new total X-axis extent (diameter, not radius) + */ @Override public void setLength(double length) { xRadius = Math.max(length / 2, Shape.EPSILON); invalidate(); } + /** + * Returns the full diameter of the ellipse along the Z axis ({@code zRadius * 2}). + * + * @return the total Z-axis extent + */ @Override public double getWidth() { return zRadius * 2; } + /** + * Sets the Z-axis semi-axis from half of {@code width}. Values below {@link Shape#EPSILON} + * are clamped. Invalidates the point cache. + * + * @param width the new total Z-axis extent (diameter, not radius) + */ @Override public void setWidth(double width) { zRadius = Math.max(width / 2, Shape.EPSILON); invalidate(); } + /** + * Returns the extrusion height along the Y axis, or {@code 0} for a flat ellipse. + * + * @return the Y-axis extrusion distance + */ @Override public double getHeight() { return height; } + /** + * Sets the extrusion height along the Y axis. A value of {@code 0} produces a flat 2-D ellipse. + * Negative values are clamped to {@code 0}. Invalidates the point cache. + * + * @param height the new Y-axis extrusion distance + */ @Override public void setHeight(double height) { this.height = Math.max(height, 0); invalidate(); } + /** + * Creates a deep copy of this ellipse, preserving radii, height, the inherited cutoff angle, + * and all {@link AbstractShape} state copied via {@link AbstractShape#copyTo(AbstractShape)}. + * + * @return a new {@link Ellipse} identical to this one + */ @Override public Shape clone() { return this.copyTo(new Ellipse(xRadius, zRadius, height)); diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Ellipsoid.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Ellipsoid.java index cab4100..32ebcee 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Ellipsoid.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Ellipsoid.java @@ -5,10 +5,21 @@ import org.joml.Quaterniond; import org.joml.Vector3d; -import java.util.LinkedHashSet; +import java.util.ArrayList; import java.util.List; -import java.util.Set; +/** + * A 3-D ellipsoid defined by three independent semi-axis radii along the X, Y, and Z axes. + *
+ * Outline sampling produces the three principal great-circle ellipses (XZ, XY, YZ planes). + * Surface sampling generates a latitude-strip decomposition: the longest horizontal cross-section + * ellipse (XZ or XY/YZ depending on which radius is larger) is used as the profile, and rings + * at each latitude are computed from the ellipsoid equation. Filled sampling nests concentric + * scaled ellipsoids to fill the interior. + *
+ * A point {@code (x, y, z)} is inside the ellipsoid when + * {@code (x/xRadius)² + (y/yRadius)² + (z/zRadius)² ≤ 1}. + */ public class Ellipsoid extends AbstractShape implements LWHShape { private static final Quaterniond XY_ROTATION = new Quaterniond().rotateX(Math.PI / 2); @@ -17,6 +28,14 @@ public class Ellipsoid extends AbstractShape implements LWHShape { protected double yRadius; protected double zRadius; + /** + * Constructs an ellipsoid with the given semi-axis lengths. Each radius is clamped to at + * least {@link Shape#EPSILON} to prevent degenerate geometry. + * + * @param xRadius semi-axis length along the X axis + * @param yRadius semi-axis length along the Y axis + * @param zRadius semi-axis length along the Z axis + */ public Ellipsoid(double xRadius, double yRadius, double zRadius) { super(); this.xRadius = Math.max(xRadius, Shape.EPSILON); @@ -24,52 +43,92 @@ public Ellipsoid(double xRadius, double yRadius, double zRadius) { this.zRadius = Math.max(zRadius, Shape.EPSILON); } + /** + * Generates the wireframe outline of the ellipsoid: three full great-circle ellipses in the + * XZ (horizontal), XY, and YZ planes. The XY and YZ ellipses are rotated into position using + * pre-computed quaternion constants so they lie correctly in their respective planes. + * + * @param points the list to which generated points are appended + * @param density the desired spacing between adjacent points along each ellipse + */ @Override - public void generateOutline(Set points, double density) { - points.addAll(Ellipse.calculateEllipse(xRadius, zRadius, density, 2 * Math.PI)); - points.addAll(VectorUtil.transform(XY_ROTATION, Ellipse.calculateEllipse(xRadius, yRadius, density, 2 * Math.PI))); - points.addAll(VectorUtil.transform(ZY_ROTATION, Ellipse.calculateEllipse(yRadius, zRadius, density, 2 * Math.PI))); + public void generateOutline(List points, double density) { + Ellipse.calculateEllipse(points, xRadius, zRadius, density, 2 * Math.PI); + int start = points.size(); + Ellipse.calculateEllipse(points, xRadius, yRadius, density, 2 * Math.PI); + VectorUtil.transform(XY_ROTATION, points, start); + start = points.size(); + Ellipse.calculateEllipse(points, yRadius, zRadius, density, 2 * Math.PI); + VectorUtil.transform(ZY_ROTATION, points, start); } + /** + * Generates points covering the surface of the ellipsoid using a latitude-strip approach. + * The longest in-plane cross-section ellipse (XY or YZ depending on whether {@code xRadius > + * zRadius}) is used as the profile from which latitude angles are derived. At each latitude + * a horizontal ring scaled by {@code cos(θ)} is added, along with its mirror below the equator. + * + * @param points the list to which generated points are appended + * @param density the desired spacing between adjacent points + */ @Override - public void generateSurface(Set points, double density) { - List ellipse; + public void generateSurface(List points, double density) { + List ellipse = new ArrayList<>(); if (xRadius > zRadius) { - ellipse = VectorUtil.transform(XY_ROTATION, Ellipse.calculateEllipse(xRadius, yRadius, density, 2 * Math.PI)); + Ellipse.calculateEllipse(ellipse, xRadius, yRadius, density, 2 * Math.PI); + VectorUtil.transform(XY_ROTATION, ellipse); } else { - ellipse = VectorUtil.transform(ZY_ROTATION, Ellipse.calculateEllipse(yRadius, zRadius, density, 2 * Math.PI)); + Ellipse.calculateEllipse(ellipse, yRadius, zRadius, density, 2 * Math.PI); + VectorUtil.transform(ZY_ROTATION, ellipse); } - points.addAll(generateEllipsoid(ellipse, 1, density)); + generateEllipsoid(points, ellipse, 1, density); } + /** + * Generates points filling the interior of the ellipsoid by nesting concentric scaled copies + * of the surface, stepping the scale factor from 1 down to 0. Each scaled shell is generated + * by the same latitude-strip method used in {@link #generateSurface}, applied to a proportionally + * scaled version of the radii. + * + * @param points the list to which generated points are appended + * @param density the desired spacing between adjacent points + */ @Override - public void generateFilled(Set points, double density) { - List ellipse; + public void generateFilled(List points, double density) { double radius = Math.max(xRadius, zRadius); int steps = (int) Math.round(radius / density); + List ellipse = new ArrayList<>(); for (int i = steps; i > 0; i--) { double r = (i / (double) steps); + ellipse.clear(); if (xRadius > zRadius) { - ellipse = VectorUtil.transform(XY_ROTATION, Ellipse.calculateEllipse(xRadius * r, yRadius * r, density, 2 * Math.PI)); + Ellipse.calculateEllipse(ellipse, xRadius * r, yRadius * r, density, 2 * Math.PI); + VectorUtil.transform(XY_ROTATION, ellipse); } else { - ellipse = VectorUtil.transform(ZY_ROTATION, Ellipse.calculateEllipse(yRadius * r, zRadius * r, density, 2 * Math.PI)); + Ellipse.calculateEllipse(ellipse, yRadius * r, zRadius * r, density, 2 * Math.PI); + VectorUtil.transform(ZY_ROTATION, ellipse); } - points.addAll(generateEllipsoid(ellipse, r, density)); + generateEllipsoid(points, ellipse, r, density); } } - private Set generateEllipsoid(List ellipse, double radius, double density) { - Set points = new LinkedHashSet<>(); + private void generateEllipsoid(List points, List ellipse, double radius, double density) { for (int i = 0; i < Math.ceil(ellipse.size() / 4.0); i++) { double y = ellipse.get(i).y; double theta = Math.asin(y / (yRadius * radius)); - for (Vector3d v2 : Ellipse.calculateEllipse(radius * xRadius * Math.cos(theta), radius * zRadius * Math.cos(theta), density, 2 * Math.PI)) { - points.add(new Vector3d(v2.x, y, v2.z)); - points.add(new Vector3d(v2.x, -y, v2.z)); + // Add ring points, setting y in-place and adding mirrored copies + int ringStart = points.size(); + Ellipse.calculateEllipse(points, radius * xRadius * Math.cos(theta), radius * zRadius * Math.cos(theta), density, 2 * Math.PI); + int ringEnd = points.size(); + for (int j = ringStart; j < ringEnd; j++) { + Vector3d v = points.get(j); + v.y = y; + if (Math.abs(y) > EPSILON) { + points.add(new Vector3d(v.x, -y, v.z)); + } } } - points.addAll(Ellipse.calculateEllipse(radius * xRadius, radius * zRadius, density, 2 * Math.PI)); - return points; + Ellipse.calculateEllipse(points, radius * xRadius, radius * zRadius, density, 2 * Math.PI); } @Override diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/EllipticalArc.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/EllipticalArc.java index faa7541..458ef97 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/EllipticalArc.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/EllipticalArc.java @@ -2,7 +2,7 @@ import org.joml.Vector3d; -import java.util.Set; +import java.util.List; public class EllipticalArc extends Ellipse implements CutoffShape { @@ -16,7 +16,7 @@ public EllipticalArc(double xRadius, double zRadius, double height, double cutof } @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { generateFilled(points, density); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Heart.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Heart.java index a82ff16..b49063f 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Heart.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Heart.java @@ -3,8 +3,8 @@ import com.sovdee.shapes.sampling.SamplingStyle; import org.joml.Vector3d; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; public class Heart extends AbstractShape implements LWHShape { @@ -19,26 +19,26 @@ public Heart(double length, double width, double eccentricity) { this.eccentricity = Math.max(eccentricity, 1); } - private static Set calculateHeart(double length, double width, double eccentricity, double density) { - Set points = new LinkedHashSet<>(); + private static void calculateHeart(List points, double length, double width, double eccentricity, double density) { double angleStep = 4 / 3.0 * density / (width + length); + if (points instanceof ArrayList al) + al.ensureCapacity(points.size() + (int) (Math.PI * 2 / angleStep) + 1); for (double theta = 0; theta < Math.PI * 2; theta += angleStep) { double x = width * Math.pow(Math.sin(theta), 3); double y = length * (Math.cos(theta) - 1 / eccentricity * Math.cos(2 * theta) - 1.0 / 6 * Math.cos(3 * theta) - 1.0 / 16 * Math.cos(4 * theta)); points.add(new Vector3d(x, 0, y)); } - return points; } @Override - public void generateOutline(Set points, double density) { - points.addAll(calculateHeart(length / 2, width / 2, eccentricity, density)); + public void generateOutline(List points, double density) { + calculateHeart(points, length / 2, width / 2, eccentricity, density); } @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { for (double w = width, l = length; w > 0 && l > 0; w -= density * 1.5, l -= density * 1.5) { - points.addAll(calculateHeart(l / 2, w / 2, eccentricity, density)); + calculateHeart(points, l / 2, w / 2, eccentricity, density); } } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Helix.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Helix.java index 4062b1e..4aa64c2 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Helix.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Helix.java @@ -3,8 +3,8 @@ import com.sovdee.shapes.sampling.SamplingStyle; import org.joml.Vector3d; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; public class Helix extends AbstractShape implements RadialShape, LWHShape { @@ -30,31 +30,31 @@ public Helix(double radius, double height, double slope, int direction) { this.direction = direction; } - private static Set calculateHelix(double radius, double height, double slope, int direction, double density) { - Set points = new LinkedHashSet<>(); + private static void calculateHelix(List points, double radius, double height, double slope, int direction, double density) { if (radius <= 0 || height <= 0) { - return points; + return; } double loops = Math.abs(height / slope); double length = slope * slope + radius * radius; double stepSize = density / length; + if (points instanceof ArrayList al) + al.ensureCapacity(points.size() + (int) (loops / stepSize) + 1); for (double t = 0; t < loops; t += stepSize) { double x = radius * Math.cos(direction * t); double z = radius * Math.sin(direction * t); points.add(new Vector3d(x, t * slope, z)); } - return points; } @Override - public void generateOutline(Set points, double density) { - points.addAll(calculateHelix(radius, height, slope, direction, density)); + public void generateOutline(List points, double density) { + calculateHelix(points, radius, height, slope, direction, density); } @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { for (double r = radius; r > 0; r -= density) { - points.addAll(calculateHelix(r, height, slope, direction, density)); + calculateHelix(points, r, height, slope, direction, density); } } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/IrregularPolygon.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/IrregularPolygon.java index 1a6092c..20147cb 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/IrregularPolygon.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/IrregularPolygon.java @@ -5,9 +5,8 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; + public class IrregularPolygon extends AbstractShape implements LWHShape { @@ -46,17 +45,18 @@ private void setBounds(Collection vertices) { } @Override - public void generateOutline(Set points, double density) { - points.addAll(Line.connectPoints(vertices, density)); - points.addAll(Line.calculateLine(vertices.get(0), vertices.get(vertices.size() - 1), density)); + public void generateOutline(List points, double density) { + int start = points.size(); + Line.connectPoints(points, vertices, density); + Line.calculateLine(points, vertices.get(0), vertices.get(vertices.size() - 1), density); if (height != 0) { - Set upperPoints = new LinkedHashSet<>(); - for (Vector3d v : points) { - upperPoints.add(new Vector3d(v.x, height, v.z)); + int end = points.size(); + for (int i = start; i < end; i++) { + Vector3d v = points.get(i); + points.add(new Vector3d(v.x, height, v.z)); } - points.addAll(upperPoints); for (Vector3d v : vertices) { - points.addAll(Line.calculateLine(v, new Vector3d(v.x, height, v.z), density)); + Line.calculateLine(points, v, new Vector3d(v.x, height, v.z), density); } } } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/LWHShape.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/LWHShape.java index 54ce72a..829bb6f 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/LWHShape.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/LWHShape.java @@ -4,10 +4,49 @@ * Represents a shape that has a length, width, and/or height. */ public interface LWHShape extends Shape { + + /** + * Returns the length of this shape along its primary axis (typically X). + * Not all implementors use all three dimensions; those that do not use length may return {@code 0}. + * + * @return the length, in the shape's local units + */ double getLength(); + + /** + * Sets the length of this shape along its primary axis. + * + * @param length the new length, in the shape's local units + */ void setLength(double length); + + /** + * Returns the width of this shape (typically along the Z axis). + * Not all implementors use all three dimensions; those that do not use width may return {@code 0}. + * + * @return the width, in the shape's local units + */ double getWidth(); + + /** + * Sets the width of this shape. + * + * @param width the new width, in the shape's local units + */ void setWidth(double width); + + /** + * Returns the height of this shape (typically along the Y axis). + * A height of {@code 0} collapses the shape to a flat, 2-D cross-section. + * + * @return the height, in the shape's local units + */ double getHeight(); + + /** + * Sets the height of this shape. A value of {@code 0} collapses the shape to a 2-D cross-section. + * + * @param height the new height, in the shape's local units; must be {@code >= 0} + */ void setHeight(double height); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Line.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Line.java index 7c830d4..8ede8bc 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Line.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Line.java @@ -3,24 +3,42 @@ import com.sovdee.shapes.sampling.SamplingStyle; import org.joml.Vector3d; -import java.util.LinkedHashSet; +import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.function.Supplier; /** - * A line shape defined by two vector endpoints. - * Supports both static endpoints and dynamic suppliers for entity-following. + * A straight-line segment defined by two vector endpoints, lying along the direction from start + * to end. The line implements {@link LWHShape} where only {@code length} (the distance between + * the endpoints) is meaningful; {@code width} and {@code height} always return {@code 0}. + *

+ * Both static and dynamic (supplier-based) endpoints are supported. Dynamic lines automatically + * mark themselves as {@link AbstractShape#isDynamic() dynamic} so the point cache is invalidated + * every frame. + *

*/ public class Line extends AbstractShape implements LWHShape { private Supplier startSupplier; private Supplier endSupplier; + /** + * Constructs a line from the origin {@code (0, 0, 0)} to the given endpoint. + * + * @param end the far endpoint of the line; must differ from the origin + */ public Line(Vector3d end) { this(new Vector3d(0, 0, 0), end); } + /** + * Constructs a static line between two explicit endpoints. + * The supplied vectors are copied so subsequent external mutations do not affect this shape. + * + * @param start the starting endpoint + * @param end the ending endpoint; must differ from {@code start} + * @throws IllegalArgumentException if {@code start} equals {@code end} + */ public Line(Vector3d start, Vector3d end) { super(); if (start.equals(end)) @@ -31,6 +49,14 @@ public Line(Vector3d start, Vector3d end) { this.endSupplier = () -> new Vector3d(e); } + /** + * Constructs a dynamic line whose endpoints are resolved fresh each frame via suppliers. + * This variant marks the shape as {@link AbstractShape#isDynamic() dynamic} so points are + * regenerated every draw call (useful for entity-following lines). + * + * @param start a supplier that returns the current start position + * @param end a supplier that returns the current end position + */ public Line(Supplier start, Supplier end) { super(); this.startSupplier = start; @@ -39,10 +65,10 @@ public Line(Supplier start, Supplier end) { } /** - * Calculates points along a line from start to end with the given density. + * Calculates points along a line from start to end with the given density, + * adding them directly to the provided list. */ - public static Set calculateLine(Vector3d start, Vector3d end, double density) { - Set points = new LinkedHashSet<>(); + public static void calculateLine(List points, Vector3d start, Vector3d end, double density) { Vector3d direction = new Vector3d(end).sub(start); double length = direction.length(); double step = length / Math.round(length / density); @@ -50,71 +76,145 @@ public static Set calculateLine(Vector3d start, Vector3d end, double d Vector3d current = new Vector3d(start); int count = (int) (length / step); + if (points instanceof ArrayList al) + al.ensureCapacity(points.size() + count + 1); for (int i = 0; i <= count; i++) { points.add(new Vector3d(current)); current.add(direction); } - return points; } /** - * Connects a list of points with lines, returning all intermediate points. + * Connects a list of control points with lines, adding all intermediate points + * directly to the provided list. Duplicate points at junctions are removed. */ - public static Set connectPoints(List points, double density) { - Set connectedPoints = new LinkedHashSet<>(); - for (int i = 0; i < points.size() - 1; i++) { - connectedPoints.addAll(calculateLine(points.get(i), points.get(i + 1), density)); + public static void connectPoints(List result, List controlPoints, double density) { + for (int i = 0; i < controlPoints.size() - 1; i++) { + int before = result.size(); + calculateLine(result, controlPoints.get(i), controlPoints.get(i + 1), density); + // Remove duplicate junction point (first point of non-first segments) + if (i > 0 && result.size() > before) { + result.remove(before); + } } - return connectedPoints; } + /** + * Generates outline points along the line from start to end at the given density. + * Delegates to {@link #calculateLine(List, Vector3d, Vector3d, double)}. + * + * @param points the list to which sampled points are appended + * @param density the approximate spacing between consecutive points + */ @Override - public void generateOutline(Set points, double density) { - points.addAll(calculateLine(getStart(), getEnd(), density)); + public void generateOutline(List points, double density) { + calculateLine(points, getStart(), getEnd(), density); } + /** + * Returns the current start position by invoking the start supplier. + * + * @return the start endpoint vector (a fresh copy for dynamic lines) + */ public Vector3d getStart() { return startSupplier.get(); } + /** + * Sets a new static start endpoint. The vector is copied internally so subsequent + * external mutations do not affect this shape. Invalidates the point cache. + * + * @param start the new start position + */ public void setStart(Vector3d start) { final Vector3d s = new Vector3d(start); this.startSupplier = () -> new Vector3d(s); invalidate(); } + /** + * Returns the current end position by invoking the end supplier. + * + * @return the end endpoint vector (a fresh copy for dynamic lines) + */ public Vector3d getEnd() { return endSupplier.get(); } + /** + * Sets a new static end endpoint. The vector is copied internally so subsequent + * external mutations do not affect this shape. Invalidates the point cache. + * + * @param end the new end position + */ public void setEnd(Vector3d end) { final Vector3d e = new Vector3d(end); this.endSupplier = () -> new Vector3d(e); invalidate(); } + /** + * Returns the raw supplier used to resolve the start position each frame. + * + * @return the start {@link Supplier} + */ public Supplier getStartSupplier() { return startSupplier; } + /** + * Replaces the start supplier without invalidating the point cache. + * Intended for dynamic-line bookkeeping; prefer {@link #setStart(Vector3d)} for + * static updates that should trigger re-sampling. + * + * @param startSupplier the new start supplier + */ public void setStartSupplier(Supplier startSupplier) { this.startSupplier = startSupplier; } + /** + * Returns the raw supplier used to resolve the end position each frame. + * + * @return the end {@link Supplier} + */ public Supplier getEndSupplier() { return endSupplier; } + /** + * Replaces the end supplier without invalidating the point cache. + * Intended for dynamic-line bookkeeping; prefer {@link #setEnd(Vector3d)} for + * static updates that should trigger re-sampling. + * + * @param endSupplier the new end supplier + */ public void setEndSupplier(Supplier endSupplier) { this.endSupplier = endSupplier; } + /** + * Computes the inter-point density required to distribute {@code targetPointCount} points + * evenly along the line. Density equals {@code length / targetPointCount}. + * + * @param style ignored; a line has no surface or fill variant + * @param targetPointCount the desired number of sampled points + * @return the spacing between consecutive points + */ @Override public double computeDensity(SamplingStyle style, int targetPointCount) { int count = Math.max(targetPointCount, 1); return new Vector3d(getEnd()).sub(getStart()).length() / count; } + /** + * Returns {@code true} if {@code point} lies on the line segment within {@link #EPSILON} + * tolerance. Uses parametric projection: {@code t = (point-start)·(end-start) / |end-start|²}, + * then checks that the closest point on the segment equals {@code point}. + * + * @param point the point to test + * @return {@code true} if the point is on the segment + */ @Override public boolean contains(Vector3d point) { Vector3d start = getStart(); @@ -129,11 +229,22 @@ public boolean contains(Vector3d point) { return point.distance(closest) <= EPSILON; } + /** + * Returns the Euclidean distance between the start and end endpoints. + * + * @return the length of the line segment + */ @Override public double getLength() { return new Vector3d(getStart()).sub(getEnd()).length(); } + /** + * Rescales the line so its total length equals {@code length} by moving the end endpoint + * along the current direction while keeping the start fixed. Invalidates the point cache. + * + * @param length the new length; clamped to at least {@link Shape#EPSILON} + */ @Override public void setLength(double length) { length = Math.max(length, Shape.EPSILON); @@ -144,18 +255,44 @@ public void setLength(double length) { setEnd(newEnd); } + /** + * Always returns {@code 0}; a line has no width dimension. + * + * @return {@code 0} + */ @Override public double getWidth() { return 0; } + /** + * No-op; a line has no width dimension. + * + * @param width ignored + */ @Override public void setWidth(double width) { } + /** + * Always returns {@code 0}; a line has no height dimension. + * + * @return {@code 0} + */ @Override public double getHeight() { return 0; } + /** + * No-op; a line has no height dimension. + * + * @param height ignored + */ @Override public void setHeight(double height) { } + /** + * Returns a deep copy of this line, preserving dynamic vs. static endpoint mode and all + * inherited {@link AbstractShape} state. + * + * @return a new {@link Line} with identical configuration + */ @Override public Shape clone() { Line clone; @@ -167,6 +304,11 @@ public Shape clone() { return this.copyTo(clone); } + /** + * Returns a human-readable description of this line including its start and end positions. + * + * @return a string of the form {@code "Line from to "} + */ public String toString() { return "Line from " + getStart() + " to " + getEnd(); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/PolyShape.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/PolyShape.java index 51a75f9..f0f38f9 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/PolyShape.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/PolyShape.java @@ -4,8 +4,37 @@ * Represents a shape that has a number of sides and a side length. */ public interface PolyShape extends Shape { + + /** + * Returns the number of sides (faces or edges) of this polygonal shape. + * Must be at least {@code 3} for a valid polygon. + * + * @return the number of sides + */ int getSides(); + + /** + * Sets the number of sides for this polygonal shape. + * Values below the minimum supported by the implementation should be clamped or rejected. + * + * @param sides the new number of sides; must be {@code >= 3} + */ void setSides(int sides); + + /** + * Returns the length of each side of this polygonal shape, in local units. + * The circumradius of a regular n-gon with side length {@code s} is + * {@code r = s / (2 * sin(π / n))}. + * + * @return the side length, in local units + */ double getSideLength(); + + /** + * Sets the side length for this polygonal shape. + * Changing the side length typically invalidates cached geometry. + * + * @param sideLength the new side length, in local units; must be {@code > 0} + */ void setSideLength(double sideLength); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RadialShape.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RadialShape.java index 3ffa825..78357a3 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RadialShape.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RadialShape.java @@ -5,6 +5,20 @@ * The radius must be greater than 0. */ public interface RadialShape extends Shape { + + /** + * Returns the radius of this shape. + * The radius is always positive (implementations should enforce {@code radius > 0}). + * + * @return the radius, in the shape's local units + */ double getRadius(); + + /** + * Sets the radius of this shape. The radius must be greater than {@code 0}; values at or + * below zero should be clamped or rejected by the implementing class. + * + * @param radius the new radius, in the shape's local units + */ void setRadius(double radius); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Rectangle.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Rectangle.java index 03f8555..0218cc7 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Rectangle.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Rectangle.java @@ -3,12 +3,23 @@ import com.sovdee.shapes.sampling.SamplingStyle; import org.joml.Vector3d; -import java.util.Set; +import java.util.List; import java.util.function.Supplier; /** - * A rectangle shape, defined by a plane and dimensions. - * For dynamic (entity-following) rectangles, use the plugin-side DynamicRectangle wrapper. + * A flat rectangular shape aligned to one of the three axis-aligned planes (XZ, XY, or YZ). + * The rectangle is centred at the origin of the shape's local coordinate space; an optional + * centre offset is applied when corners are supplied, shifting the origin to the midpoint of + * the two corners. + *

+ * Implements {@link LWHShape} where {@code length} and {@code width} describe the two in-plane + * dimensions; {@code height} is always {@code 0}. The orientation plane is chosen via the + * {@link Plane} enum. + *

+ *

+ * For entity-following rectangles whose corners track moving locations, use the plugin-side + * {@code DynamicRectangle} wrapper rather than the supplier constructor directly. + *

*/ public class Rectangle extends AbstractShape implements LWHShape { @@ -21,6 +32,13 @@ public class Rectangle extends AbstractShape implements LWHShape { private Supplier cornerASupplier; private Supplier cornerBSupplier; + /** + * Constructs a rectangle centred at the local origin with explicit dimensions. + * + * @param length the extent along the first axis of {@code plane}; clamped to at least {@link Shape#EPSILON} + * @param width the extent along the second axis of {@code plane}; clamped to at least {@link Shape#EPSILON} + * @param plane the axis-aligned plane in which the rectangle lies + */ public Rectangle(double length, double width, Plane plane) { super(); this.plane = plane; @@ -28,6 +46,16 @@ public Rectangle(double length, double width, Plane plane) { this.halfWidth = Math.max(width / 2, Shape.EPSILON); } + /** + * Constructs a static rectangle from two opposite corner positions. + * The centre offset is computed from the midpoint of the two corners (projected onto + * {@code plane} so the off-axis component is zeroed). + * + * @param cornerA the first corner of the rectangle + * @param cornerB the diagonally opposite corner; must differ from {@code cornerA} + * @param plane the axis-aligned plane in which the rectangle lies + * @throws IllegalArgumentException if {@code cornerA} equals {@code cornerB} + */ public Rectangle(Vector3d cornerA, Vector3d cornerB, Plane plane) { super(); if (cornerA.equals(cornerB)) @@ -42,6 +70,15 @@ public Rectangle(Vector3d cornerA, Vector3d cornerB, Plane plane) { } } + /** + * Constructs a dynamic rectangle whose corners are resolved each frame via suppliers. + * The shape is marked as {@link AbstractShape#isDynamic() dynamic} so points are regenerated + * every draw call. The initial dimensions are computed from the suppliers' first values. + * + * @param cornerA a supplier providing the current position of the first corner + * @param cornerB a supplier providing the current position of the diagonally opposite corner + * @param plane the axis-aligned plane in which the rectangle lies + */ public Rectangle(Supplier cornerA, Supplier cornerB, Plane plane) { super(); this.plane = plane; @@ -79,6 +116,13 @@ private void calculateSteps(double density) { widthStep = 2 * halfLength / Math.round(2 * halfLength / density); } + /** + * Called before each sampling pass. If dynamic corner suppliers are present, refreshes the + * half-length and half-width from their current values. Then pre-computes the per-axis step + * sizes used by {@link #generateOutline} and {@link #generateSurface}. + * + * @param density the target spacing between sampled points + */ @Override public void beforeSampling(double density) { if (cornerASupplier != null && cornerBSupplier != null) { @@ -89,13 +133,27 @@ public void beforeSampling(double density) { calculateSteps(density); } + /** + * Called after each sampling pass to apply the centre offset to all generated points. + * This translates the points (which are generated relative to the local origin) into the + * correct world position when the rectangle was constructed from corners. + * + * @param points the list of sampled points to translate in-place + */ @Override - public void afterSampling(Set points) { + public void afterSampling(List points) { points.forEach(vector -> vector.add(centerOffset)); } + /** + * Generates outline (perimeter) points for the rectangle. Points are placed along all four + * edges at the step sizes calculated in {@link #beforeSampling}. + * + * @param points the list to which outline points are appended + * @param density the approximate spacing between consecutive points (used implicitly via step sizes) + */ @Override - public void generateOutline(Set points, double density) { + public void generateOutline(List points, double density) { for (double l = -halfLength + widthStep; l < halfLength; l += widthStep) { points.add(vectorFromLengthWidth(l, -halfWidth)); points.add(vectorFromLengthWidth(l, halfWidth)); @@ -106,8 +164,15 @@ public void generateOutline(Set points, double density) { } } + /** + * Generates surface (filled plane) points for the rectangle by sampling a uniform grid + * across both in-plane axes. + * + * @param points the list to which surface points are appended + * @param density the approximate spacing between consecutive points (used implicitly via step sizes) + */ @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { for (double w = -halfWidth; w <= halfWidth; w += lengthStep) { for (double l = -halfLength; l <= halfLength; l += widthStep) { points.add(vectorFromLengthWidth(l, w)); @@ -115,6 +180,14 @@ public void generateSurface(Set points, double density) { } } + /** + * Computes the inter-point density needed to reach {@code targetPointCount} sampled points. + * For SURFACE/FILL: {@code sqrt(area / count)}. For OUTLINE: {@code perimeter / count}. + * + * @param style the sampling style that determines which formula is used + * @param targetPointCount the desired number of points + * @return the computed density + */ @Override public double computeDensity(SamplingStyle style, int targetPointCount) { int count = Math.max(targetPointCount, 1); @@ -124,6 +197,13 @@ public double computeDensity(SamplingStyle style, int targetPointCount) { }; } + /** + * Returns {@code true} if {@code point} lies within (or on the boundary of) the rectangle, + * including the constraint that it lies on the rectangle's plane (within {@link #EPSILON}). + * + * @param point the point to test in local coordinates + * @return {@code true} if the point is inside the rectangle + */ @Override public boolean contains(Vector3d point) { return switch (plane) { @@ -133,40 +213,99 @@ public boolean contains(Vector3d point) { }; } + /** + * Returns the full length of the rectangle (twice the internal half-length). + * + * @return the length along the first in-plane axis + */ @Override public double getLength() { return halfLength * 2; } + /** + * Sets the rectangle's length and invalidates the point cache. + * + * @param length the new full length; clamped to at least {@link Shape#EPSILON} + */ @Override public void setLength(double length) { this.halfLength = Math.max(length / 2, Shape.EPSILON); invalidate(); } + /** + * Returns the full width of the rectangle (twice the internal half-width). + * + * @return the width along the second in-plane axis + */ @Override public double getWidth() { return halfWidth * 2; } + /** + * Sets the rectangle's width and invalidates the point cache. + * + * @param width the new full width; clamped to at least {@link Shape#EPSILON} + */ @Override public void setWidth(double width) { this.halfWidth = Math.max(width / 2, Shape.EPSILON); invalidate(); } + /** + * Always returns {@code 0}; a rectangle has no height dimension. + * + * @return {@code 0} + */ @Override public double getHeight() { return 0; } + /** + * No-op; a rectangle has no height dimension. + * + * @param height ignored + */ @Override public void setHeight(double height) { } + /** + * Returns the axis-aligned {@link Plane} in which this rectangle lies. + * + * @return the current plane + */ public Plane getPlane() { return plane; } + /** + * Sets the axis-aligned plane and invalidates the point cache. + * + * @param plane the new plane orientation + */ public void setPlane(Plane plane) { this.plane = plane; invalidate(); } + /** + * Returns the supplier used to resolve the first corner for dynamic rectangles, or + * {@code null} if the rectangle was constructed from static dimensions or corners. + * + * @return the corner A supplier, or {@code null} + */ public Supplier getCornerASupplier() { return cornerASupplier; } + + /** + * Returns the supplier used to resolve the second corner for dynamic rectangles, or + * {@code null} if the rectangle was constructed from static dimensions or corners. + * + * @return the corner B supplier, or {@code null} + */ public Supplier getCornerBSupplier() { return cornerBSupplier; } + /** + * Returns a deep copy of this rectangle, preserving dynamic vs. static corner mode, the + * centre offset, and all inherited {@link AbstractShape} state. + * + * @return a new {@link Rectangle} with identical configuration + */ @Override public Shape clone() { Rectangle rectangle; @@ -179,13 +318,37 @@ public Shape clone() { return this.copyTo(rectangle); } + /** + * Returns a human-readable description including the plane name, length, and width. + * + * @return a string of the form {@code " rectangle with length and width "} + */ @Override public String toString() { String axis = this.plane.toString().toLowerCase(); return axis + " rectangle with length " + this.getLength() + " and width " + this.getWidth(); } + /** + * Identifies which of the three axis-aligned planes a {@link Rectangle} lies in. + *
    + *
  • {@link #XZ} - the horizontal ground plane (Y = 0)
  • + *
  • {@link #XY} - the vertical frontal plane (Z = 0)
  • + *
  • {@link #YZ} - the vertical lateral plane (X = 0)
  • + *
+ */ public enum Plane { - XZ, XY, YZ + /** + * The horizontal XZ plane; length along X, width along Z. + */ + XZ, + /** + * The vertical XY plane; length along X, width along Y. + */ + XY, + /** + * The vertical YZ plane; length along Y, width along Z. + */ + YZ } } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RegularPolygon.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RegularPolygon.java index 87705b3..e618eac 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RegularPolygon.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RegularPolygon.java @@ -4,27 +4,71 @@ import com.sovdee.shapes.util.VectorUtil; import org.joml.Vector3d; -import java.util.LinkedHashSet; -import java.util.Set; - +import java.util.ArrayList; +import java.util.List; + +/** + * A regular polygon (or regular prism when {@code height > 0}) with equal-length sides and equal + * interior angles, oriented in the XZ plane. The circumscribed circle has the given {@code radius}, + * and each side subtends an angle of {@code 2π / sides} at the centre. + *

+ * When {@code height == 0} the shape is a flat polygon; when {@code height > 0} it becomes a + * prism extruded along the Y axis. The shape implements {@link PolyShape}, {@link RadialShape}, + * and {@link LWHShape}; {@code length} and {@code width} from {@link LWHShape} always return + * {@code 0} for this shape — only {@code height} is meaningful. + *

+ *

+ * The minimum number of sides is 3 (triangle); the maximum interior angle per side is + * {@code 2π/3} (i.e. a minimum of 3 sides is enforced when setting via angle). + *

+ */ public class RegularPolygon extends AbstractShape implements PolyShape, RadialShape, LWHShape { private double angle; private double radius; private double height; + /** + * Constructs a flat regular polygon (height = 0) with the given number of sides and radius. + * + * @param sides the number of sides; determines the vertex angle as {@code 2π / sides} + * @param radius the circumscribed (outer) radius; clamped to at least {@link Shape#EPSILON} + */ public RegularPolygon(int sides, double radius) { this((Math.PI * 2) / sides, radius, 0); } + /** + * Constructs a flat regular polygon (height = 0) with an explicit vertex angle and radius. + * The number of sides is derived as {@code floor(2π / angle)}. + * + * @param angle the angular size of each side in radians; clamped to {@code [ε, 2π/3]} + * @param radius the circumscribed (outer) radius; clamped to at least {@link Shape#EPSILON} + */ public RegularPolygon(double angle, double radius) { this(angle, radius, 0); } + /** + * Constructs a regular prism with the given number of sides, circumscribed radius, and height. + * When {@code height == 0} this is equivalent to {@link #RegularPolygon(int, double)}. + * + * @param sides the number of sides + * @param radius the circumscribed (outer) radius; clamped to at least {@link Shape#EPSILON} + * @param height the extrusion height along the Y axis; {@code 0} for a flat polygon + */ public RegularPolygon(int sides, double radius, double height) { this((Math.PI * 2) / sides, radius, height); } + /** + * Constructs a regular prism with an explicit vertex angle, radius, and height. + * This is the canonical constructor; all other constructors delegate here. + * + * @param angle the angular size of each side in radians; clamped to {@code [ε, 2π/3]} + * @param radius the circumscribed (outer) radius; clamped to at least {@link Shape#EPSILON} + * @param height the extrusion height along the Y axis; {@code 0} for a flat polygon + */ public RegularPolygon(double angle, double radius, double height) { super(); this.angle = Math.clamp(angle, Shape.EPSILON, Math.PI * 2 / 3); @@ -34,10 +78,25 @@ public RegularPolygon(double angle, double radius, double height) { // --- Static calculation methods --- - public static Set calculateRegularPolygon(double radius, double angle, double density, boolean wireframe) { + /** + * Samples points on (or filling) a regular polygon in the XZ plane. + *

+ * When {@code wireframe} is {@code true} only the perimeter edges are sampled. When + * {@code false}, concentric rings from the outer radius inward are filled (surface mode), + * with a centre point added explicitly. + *

+ * The apothem is {@code radius * cos(angle/2)}; the radial step between rings is + * {@code radius / round(apothem / density)}. + * + * @param points the list to which sampled points are appended + * @param radius the circumscribed radius of the polygon + * @param angle the angular size of each side in radians ({@code 2π / sides}) + * @param density the approximate spacing between consecutive points + * @param wireframe {@code true} to sample only the outer perimeter; {@code false} to fill + */ + public static void calculateRegularPolygon(List points, double radius, double angle, double density, boolean wireframe) { angle = Math.max(angle, Shape.EPSILON); - Set points = new LinkedHashSet<>(); double apothem = radius * Math.cos(angle / 2); double radiusStep = radius / Math.round(apothem / density); if (wireframe) { @@ -48,63 +107,118 @@ public static Set calculateRegularPolygon(double radius, double angle, for (double subRadius = radius; subRadius >= 0; subRadius -= radiusStep) { Vector3d vertex = new Vector3d(subRadius, 0, 0); for (double i = 0; i < 2 * Math.PI; i += angle) { - points.addAll(Line.calculateLine( + Line.calculateLine(points, VectorUtil.rotateAroundY(new Vector3d(vertex), i), VectorUtil.rotateAroundY(new Vector3d(vertex), i + angle), - density)); + density); } } - return points; } - public static Set calculateRegularPrism(double radius, double angle, double height, double density, boolean wireframe) { - Set points = new LinkedHashSet<>(); + /** + * Samples points on (or filling) a regular prism extruded along the Y axis. + *

+ * For each side of the polygon, the bottom edge is sampled using + * {@link Line#calculateLine(List, Vector3d, Vector3d, double)}, then either vertical edge + * points are paired (wireframe) or filled columns are generated up to {@code height}. + * Vertical pillar edges at each vertex are always included in wireframe mode. + *

+ * + * @param points the list to which sampled points are appended + * @param radius the circumscribed radius of the prism's base + * @param angle the angular size of each side in radians ({@code 2π / sides}) + * @param height the extrusion height along the Y axis + * @param density the approximate spacing between consecutive points + * @param wireframe {@code true} to sample only the edges; {@code false} to fill the lateral faces + */ + public static void calculateRegularPrism(List points, double radius, double angle, double height, double density, boolean wireframe) { Vector3d vertex = new Vector3d(radius, 0, 0); + // Need a temp list for the edge points since each spawns vertical points + List edgePoints = new ArrayList<>(); for (double i = 0; i < 2 * Math.PI; i += angle) { Vector3d currentVertex = VectorUtil.rotateAroundY(new Vector3d(vertex), i); - for (Vector3d vector : Line.calculateLine(currentVertex, VectorUtil.rotateAroundY(new Vector3d(vertex), i + angle), density)) { + edgePoints.clear(); + Line.calculateLine(edgePoints, currentVertex, VectorUtil.rotateAroundY(new Vector3d(vertex), i + angle), density); + for (Vector3d vector : edgePoints) { points.add(vector); if (wireframe) { points.add(new Vector3d(vector.x, height, vector.z)); } else { - points.addAll(Line.calculateLine(vector, new Vector3d(vector.x, height, vector.z), density)); + Line.calculateLine(points, vector, new Vector3d(vector.x, height, vector.z), density); } } if (wireframe) - points.addAll(Line.calculateLine(currentVertex, new Vector3d(currentVertex.x, height, currentVertex.z), density)); + Line.calculateLine(points, currentVertex, new Vector3d(currentVertex.x, height, currentVertex.z), density); } - return points; } // --- Generation methods --- + /** + * Generates outline (perimeter) points. For flat polygons this is the outer edge; for prisms + * this includes both the top/bottom edges and the vertical pillar edges at each vertex. + * + * @param points the list to which outline points are appended + * @param density the approximate spacing between consecutive points + */ @Override - public void generateOutline(Set points, double density) { + public void generateOutline(List points, double density) { if (height == 0) - points.addAll(calculateRegularPolygon(this.radius, this.angle, density, true)); + calculateRegularPolygon(points, this.radius, this.angle, density, true); else - points.addAll(calculateRegularPrism(this.radius, this.angle, this.height, density, true)); + calculateRegularPrism(points, this.radius, this.angle, this.height, density, true); } + /** + * Generates surface points. For flat polygons this samples the filled face; for prisms this + * samples the lateral faces (not the top/bottom caps). + * + * @param points the list to which surface points are appended + * @param density the approximate spacing between consecutive points + */ @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { if (height == 0) - points.addAll(calculateRegularPolygon(this.radius, this.angle, density, false)); + calculateRegularPolygon(points, this.radius, this.angle, density, false); else - points.addAll(calculateRegularPrism(this.radius, this.angle, this.height, density, false)); + calculateRegularPrism(points, this.radius, this.angle, this.height, density, false); } + /** + * Generates filled interior points. For flat polygons this is identical to surface mode. + * For prisms, a surface polygon is generated and then replicated vertically using + * {@link AbstractShape#fillVertically} to fill the volume. + * + * @param points the list to which filled points are appended + * @param density the approximate spacing between consecutive points + */ @Override - public void generateFilled(Set points, double density) { + public void generateFilled(List points, double density) { if (height == 0) generateSurface(points, density); else { - Set polygon = calculateRegularPolygon(this.radius, this.angle, density, false); - fillVertically(polygon, height, density); - points.addAll(polygon); + int start = points.size(); + calculateRegularPolygon(points, this.radius, this.angle, density, false); + fillVertically(points, start, height, density); } } + /** + * Computes the density needed to achieve {@code targetPointCount} sampled points. + * Uses the following formulas (where {@code s = sides}, {@code r = radius}, + * {@code θ = angle}, {@code h = height}): + *
    + *
  • OUTLINE (flat): {@code 2·s·r·sin(θ/2) / count}
  • + *
  • OUTLINE (prism): {@code (4·s·r·sin(θ/2) + h·s) / count}
  • + *
  • SURFACE (flat): {@code sqrt(s·r²·sin(θ)/2 / count)}
  • + *
  • SURFACE (prism): {@code (s·r²·sin(θ) + sideLength·s·h) / count}
  • + *
  • FILL: {@code s·r²·sin(θ)·h / count}
  • + *
+ * + * @param style determines which area/perimeter formula to apply + * @param targetPointCount the desired number of points + * @return the computed density + */ @Override public double computeDensity(SamplingStyle style, int targetPointCount) { int count = Math.max(targetPointCount, 1); @@ -124,6 +238,15 @@ public double computeDensity(SamplingStyle style, int targetPointCount) { }; } + /** + * Returns {@code true} if {@code point} lies within the polygon (or prism) in local + * coordinates. For flat polygons the point must lie in the XZ plane (|y| < ε). For prisms + * the Y coordinate must be in {@code [0, height]}. The XZ containment check uses the apothem + * and the angular position to test against the actual polygon boundary. + * + * @param point the point to test in local coordinates + * @return {@code true} if the point is inside the polygon or prism + */ @Override public boolean contains(Vector3d point) { if (height > 0 && (point.y < 0 || point.y > height)) return false; @@ -134,18 +257,40 @@ public boolean contains(Vector3d point) { return dist <= radius && dist <= apothem / Math.cos(Math.atan2(point.z, point.x) % angle - angle / 2); } + /** + * Returns the number of sides, computed as {@code floor(2π / angle)}. + * + * @return the number of sides of this polygon + */ @Override public int getSides() { return (int) (Math.PI * 2 / this.angle); } + /** + * Sets the number of sides by recomputing the vertex angle as {@code 2π / sides}. + * Minimum of 3 sides is enforced. Invalidates the point cache. + * + * @param sides the desired number of sides; clamped to at least 3 + */ @Override public void setSides(int sides) { this.angle = (Math.PI * 2) / Math.max(sides, 3); invalidate(); } + /** + * Returns the edge length of each side, equal to {@code 2 * radius * sin(angle / 2)}. + * + * @return the side length + */ @Override public double getSideLength() { return this.radius * 2 * Math.sin(this.angle / 2); } + /** + * Sets the radius so that the edge length equals {@code sideLength}, keeping the current + * angle (and thus side count) unchanged. Invalidates the point cache. + * + * @param sideLength the desired edge length; clamped to at least {@link Shape#EPSILON} + */ @Override public void setSideLength(double sideLength) { sideLength = Math.max(sideLength, Shape.EPSILON); @@ -154,41 +299,93 @@ public void setSideLength(double sideLength) { invalidate(); } + /** + * Returns the circumscribed (outer) radius of the polygon. + * + * @return the radius + */ @Override public double getRadius() { return this.radius; } + /** + * Sets the circumscribed radius and invalidates the point cache. + * + * @param radius the new radius; clamped to at least {@link Shape#EPSILON} + */ @Override public void setRadius(double radius) { this.radius = Math.max(radius, Shape.EPSILON); invalidate(); } + /** + * Always returns {@code 0}; length is not a distinct dimension of this shape. + * + * @return {@code 0} + */ @Override public double getLength() { return 0; } + /** + * No-op; length is not a distinct dimension of this shape. + * + * @param length ignored + */ @Override public void setLength(double length) { } + /** + * Always returns {@code 0}; width is not a distinct dimension of this shape. + * + * @return {@code 0} + */ @Override public double getWidth() { return 0; } + /** + * No-op; width is not a distinct dimension of this shape. + * + * @param width ignored + */ @Override public void setWidth(double width) { } + /** + * Returns the extrusion height of the prism along the Y axis, or {@code 0} for a flat polygon. + * + * @return the height + */ @Override public double getHeight() { return height; } + /** + * Sets the extrusion height and invalidates the point cache. A value of {@code 0} produces a + * flat polygon; positive values produce a prism. + * + * @param height the new height; clamped to at least {@code 0} + */ @Override public void setHeight(double height) { this.height = Math.max(height, 0); invalidate(); } + /** + * Returns a deep copy of this polygon, preserving angle, radius, height, and all inherited + * {@link AbstractShape} state. + * + * @return a new {@link RegularPolygon} with identical configuration + */ @Override public Shape clone() { return this.copyTo(new RegularPolygon(angle, radius, height)); } + /** + * Returns a human-readable description of this polygon including side count and radius. + * + * @return a string of the form {@code "regular polygon with sides and radius "} + */ @Override public String toString() { return "regular polygon with " + getSides() + " sides and radius " + getRadius(); diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RegularPolyhedron.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RegularPolyhedron.java index ec0042b..69d0696 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RegularPolyhedron.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/RegularPolyhedron.java @@ -4,10 +4,23 @@ import org.joml.Quaterniond; import org.joml.Vector3d; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.List; +/** + * One of the five Platonic solids (regular convex polyhedra), specifically the subset with + * triangular or pentagonal faces: tetrahedron (4 faces), octahedron (8 faces), dodecahedron + * (12 faces), and icosahedron (20 faces). The shape is centred at the origin and scaled so that + * the circumscribed sphere has the given {@code radius}. + *

+ * Each face is generated independently using pre-computed quaternion rotations ({@code TETRAHEDRON_FACES}, + * etc.) that orient a canonical face into its correct position. Surface and outline points per face + * are produced via {@link RegularPolygon#calculateRegularPolygon}. + *

+ *

+ * The {@link #contains(Vector3d)} check uses a conservative inscribed-sphere test rather than an + * exact half-plane test. + *

+ */ public class RegularPolyhedron extends AbstractShape implements RadialShape, PolyShape { private static final Quaterniond[] TETRAHEDRON_FACES = { @@ -72,6 +85,13 @@ public class RegularPolyhedron extends AbstractShape implements RadialShape, Pol private double radius; private int faces; + /** + * Constructs a regular polyhedron with the given circumscribed radius and face count. + * Only face counts of 4, 8, 12, and 20 are valid; any other value defaults to 4 (tetrahedron). + * + * @param radius the circumscribed sphere radius; clamped to at least {@link Shape#EPSILON} + * @param faces the number of faces; must be 4, 8, 12, or 20 (defaults to 4 otherwise) + */ public RegularPolyhedron(double radius, int faces) { super(); this.radius = Math.max(radius, Shape.EPSILON); @@ -81,45 +101,62 @@ public RegularPolyhedron(double radius, int faces) { }; } + /** + * Generates wireframe (edge) outline points for each face of the polyhedron by applying + * the face-specific quaternion rotation to outline points of a regular polygon face. + * + * @param points the list to which outline points are appended + * @param density the approximate spacing between consecutive points + */ @Override - public void generateOutline(Set points, double density) { - points.addAll(switch (faces) { - case 4 -> generatePolyhedron(TETRAHEDRON_FACES, radius, density, SamplingStyle.OUTLINE); - case 8 -> generatePolyhedron(OCTAHEDRON_FACES, radius, density, SamplingStyle.OUTLINE); - case 20 -> generatePolyhedron(ICOSAHEDRON_FACES, radius, density, SamplingStyle.OUTLINE); - case 12 -> generatePolyhedron(DODECAHEDRON_FACES, radius, density, SamplingStyle.OUTLINE); - default -> new HashSet<>(); - }); + public void generateOutline(List points, double density) { + Quaterniond[] rotations = getFaceRotations(); + if (rotations != null) + generatePolyhedron(points, rotations, radius, density, SamplingStyle.OUTLINE); } + /** + * Generates surface points covering all faces of the polyhedron. Each face is filled + * with a concentric-ring polygon pattern and then rotated into place. + * + * @param points the list to which surface points are appended + * @param density the approximate spacing between consecutive points + */ @Override - public void generateSurface(Set points, double density) { - points.addAll(switch (faces) { - case 4 -> generatePolyhedron(TETRAHEDRON_FACES, radius, density, SamplingStyle.SURFACE); - case 8 -> generatePolyhedron(OCTAHEDRON_FACES, radius, density, SamplingStyle.SURFACE); - case 20 -> generatePolyhedron(ICOSAHEDRON_FACES, radius, density, SamplingStyle.SURFACE); - case 12 -> generatePolyhedron(DODECAHEDRON_FACES, radius, density, SamplingStyle.SURFACE); - default -> new HashSet<>(); - }); + public void generateSurface(List points, double density) { + Quaterniond[] rotations = getFaceRotations(); + if (rotations != null) + generatePolyhedron(points, rotations, radius, density, SamplingStyle.SURFACE); } + /** + * Generates filled interior points by stacking concentric scaled copies of the surface from + * {@code radius} down to {@code 0} in steps of {@code radius / round(radius / density)}. + * + * @param points the list to which filled points are appended + * @param density the approximate spacing between consecutive points + */ @Override - public void generateFilled(Set points, double density) { + public void generateFilled(List points, double density) { double step = radius / Math.round(radius / density); - Quaterniond[] rotations = switch (faces) { + Quaterniond[] rotations = getFaceRotations(); + if (rotations == null) return; + for (double i = radius; i > 0; i -= step) { + generatePolyhedron(points, rotations, i, density, SamplingStyle.SURFACE); + } + } + + private Quaterniond[] getFaceRotations() { + return switch (faces) { case 4 -> TETRAHEDRON_FACES; case 8 -> OCTAHEDRON_FACES; case 12 -> DODECAHEDRON_FACES; case 20 -> ICOSAHEDRON_FACES; - default -> new Quaterniond[0]; + default -> null; }; - for (double i = radius; i > 0; i -= step) { - points.addAll(generatePolyhedron(rotations, i, density, SamplingStyle.SURFACE)); - } } - private Set generatePolyhedron(Quaterniond[] rotations, double radius, double density, SamplingStyle style) { - Set points = new LinkedHashSet<>(); + private void generatePolyhedron(List points, Quaterniond[] rotations, double radius, double density, SamplingStyle style) { int sides = this.faces == 12 ? 5 : 3; double sideLength = switch (faces) { case 4 -> radius / TETRA_R2SL; @@ -138,29 +175,24 @@ private Set generatePolyhedron(Quaterniond[] rotations, double radius, Vector3d offset = new Vector3d(0, inscribedRadius, 0); double faceRadius = sideLength / (2 * Math.sin(Math.PI / sides)); for (Quaterniond rotation : rotations) { - Set facePoints = new LinkedHashSet<>(switch (style) { - case OUTLINE -> generateFaceOutline(sides, faceRadius, density); - case FILL, SURFACE -> generateFaceSurface(sides, faceRadius, density); - }); - facePoints.forEach(point -> rotation.transform(point.add(offset))); - points.addAll(facePoints); + int faceStart = points.size(); + switch (style) { + case OUTLINE -> RegularPolygon.calculateRegularPolygon(points, faceRadius, 2 * Math.PI / sides, density, true); + case FILL, SURFACE -> generateFaceSurface(points, sides, faceRadius, density); + } + for (int i = faceStart; i < points.size(); i++) { + rotation.transform(points.get(i).add(offset)); + } } - return points; - } - - private Set generateFaceOutline(int sides, double radius, double density) { - return new LinkedHashSet<>(RegularPolygon.calculateRegularPolygon(radius, 2 * Math.PI / sides, density, true)); } - private Set generateFaceSurface(int sides, double radius, double density) { - Set facePoints = new LinkedHashSet<>(); + private void generateFaceSurface(List points, int sides, double radius, double density) { double apothem = radius * Math.cos(Math.PI / sides); double radiusStep = radius / Math.round(apothem / density); for (double subRadius = radius; subRadius > 0; subRadius -= radiusStep) { - facePoints.addAll(RegularPolygon.calculateRegularPolygon(subRadius, 2 * Math.PI / sides, density, false)); + RegularPolygon.calculateRegularPolygon(points, subRadius, 2 * Math.PI / sides, density, false); } - facePoints.add(new Vector3d(0, 0, 0)); - return facePoints; + points.add(new Vector3d(0, 0, 0)); } @Override diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Shape.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Shape.java index 194910c..d3238d3 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Shape.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Shape.java @@ -1,11 +1,13 @@ package com.sovdee.shapes.shapes; +import com.sovdee.shapes.modifiers.PointContext; import com.sovdee.shapes.sampling.PointSampler; import com.sovdee.shapes.sampling.SamplingStyle; +import com.sovdee.shapes.sampling.ShapeRenderer; import org.joml.Quaterniond; import org.joml.Vector3d; -import java.util.Set; +import java.util.List; /** * Represents a geometric shape. Pure geometry interface — sampling/caching/drawing @@ -17,39 +19,154 @@ public interface Shape extends Cloneable { // --- Spatial transform --- + /** + * Returns a copy of this shape's orientation quaternion. + * The orientation describes the rotation applied to all generated points before they are + * returned to the caller; it does not affect the shape's local geometry. + * + * @return a defensive copy of the current {@link Quaterniond} orientation + */ Quaterniond getOrientation(); + + /** + * Sets the orientation of this shape. + * The provided quaternion is copied, so subsequent mutation of the argument has no effect. + * + * @param orientation the new orientation quaternion; must not be null + */ void setOrientation(Quaterniond orientation); + /** + * Returns the uniform scale factor applied to this shape's generated points. + * A value of {@code 1.0} means no scaling; values greater than {@code 1.0} enlarge the shape. + * + * @return the current scale factor + */ double getScale(); + + /** + * Sets the uniform scale factor for this shape. + * The scale is applied to every generated point relative to the shape's local origin. + * + * @param scale the new scale factor; typically positive + */ void setScale(double scale); + /** + * Returns a copy of this shape's positional offset vector. + * The offset is added to every generated point after orientation and scale are applied, + * shifting the entire shape in local space. + * + * @return a defensive copy of the current offset {@link Vector3d} + */ Vector3d getOffset(); + + /** + * Sets the positional offset applied to this shape's generated points. + * + * @param offset the new offset vector; must not be null + */ void setOffset(Vector3d offset); // --- Oriented axes --- + /** + * Returns the world-space direction of this shape's local X axis after applying the current orientation. + * Equivalent to rotating {@code (1, 0, 0)} by {@link #getOrientation()}. + * + * @return the transformed X axis as a unit {@link Vector3d} + */ Vector3d getRelativeXAxis(); + + /** + * Returns the world-space direction of this shape's local Y axis after applying the current orientation. + * Equivalent to rotating {@code (0, 1, 0)} by {@link #getOrientation()}. + * + * @return the transformed Y axis as a unit {@link Vector3d} + */ Vector3d getRelativeYAxis(); + + /** + * Returns the world-space direction of this shape's local Z axis after applying the current orientation. + * Equivalent to rotating {@code (0, 0, 1)} by {@link #getOrientation()}. + * + * @return the transformed Z axis as a unit {@link Vector3d} + */ Vector3d getRelativeZAxis(); // --- Geometry query --- + /** + * Returns {@code true} if the given point lies within or on the surface of this shape's volume. + * Points are tested in the shape's local coordinate space (before orientation and scale are applied). + * Implementations should use {@link #EPSILON} for floating-point boundary comparisons. + * + * @param point the point to test, in local coordinates + * @return {@code true} if the point is contained by this shape + */ boolean contains(Vector3d point); // --- Change detection --- + /** + * Returns a monotonically increasing version counter that is incremented each time the shape's + * geometry changes (e.g. when a radius or height setter is called). {@link PointSampler} + * implementations use this value to detect stale caches without performing a deep comparison. + * + * @return the current version number; starts at {@code 0} and only increases + */ long getVersion(); // --- Dynamic support --- + /** + * Returns {@code true} if this shape is dynamic, meaning its geometry may change between + * render frames (e.g. a Bezier curve driven by a supplier of live locations). + * Dynamic shapes bypass the point cache and re-generate points every frame. + * + * @return {@code true} if this shape is marked dynamic + */ boolean isDynamic(); + + /** + * Marks this shape as dynamic or static. Dynamic shapes skip the {@link PointSampler} + * cache and regenerate points on every render call. + * + * @param dynamic {@code true} to enable dynamic mode; {@code false} to enable caching + */ void setDynamic(boolean dynamic); // --- Point generation (density as parameter) --- - void generateOutline(Set points, double density); - void generateSurface(Set points, double density); - void generateFilled(Set points, double density); + /** + * Generates points that trace the wireframe outline of this shape and appends them to {@code points}. + * For most shapes this produces one or more closed loops of evenly spaced points. + * The approximate arc-length distance between consecutive points equals {@code density}. + * + * @param points the list to append generated points to; must not be null + * @param density the desired spacing between points; smaller values produce more points + */ + void generateOutline(List points, double density); + + /** + * Generates points that cover the surface of this shape and appends them to {@code points}. + * The surface is the outermost shell, distinct from the filled interior. + * Falls back to {@link #generateOutline} in {@link AbstractShape} unless overridden. + * + * @param points the list to append generated points to; must not be null + * @param density the desired spacing between points + */ + void generateSurface(List points, double density); + + /** + * Generates points that fill the volume of this shape and appends them to {@code points}. + * The result includes both the surface and interior points. + * Falls back to {@link #generateSurface} in {@link AbstractShape} unless overridden. + * + * @param points the list to append generated points to; must not be null + * @param density the desired spacing between points; smaller values produce denser fills + */ + void generateFilled(List points, double density); /** * Called by PointSampler before point generation. Override for supplier refresh, step recalc, etc. @@ -59,7 +176,7 @@ default void beforeSampling(double density) {} /** * Called by PointSampler after point generation. Override for centerOffset adjustment, etc. */ - default void afterSampling(Set points) {} + default void afterSampling(List points) {} /** * Computes the density needed to achieve approximately the given number of points. @@ -68,11 +185,47 @@ default void afterSampling(Set points) {} // --- PointSampler --- + /** + * Returns the {@link PointSampler} responsible for caching, ordering, and modifying this + * shape's generated points. Each shape owns exactly one sampler. + * + * @return the current {@link PointSampler}; never null after construction + */ PointSampler getPointSampler(); + + /** + * Replaces the {@link PointSampler} used by this shape. + * The new sampler takes over all caching and render-modifier management immediately. + * + * @param sampler the new sampler to use; must not be null + */ void setPointSampler(PointSampler sampler); + /** + * Convenience default: drives the full render loop using the shape's own orientation. + */ + default void render(Quaterniond orientation, ShapeRenderer renderer) { + getPointSampler().render(this, orientation, renderer); + } + // --- Replication --- + /** + * Creates and returns a deep copy of this shape, including its spatial transform (orientation, + * scale, offset), dynamic flag, and point sampler. + * Implementations must return the most specific concrete type possible. + * + * @return a new, independent {@link Shape} with identical state + */ Shape clone(); + + /** + * Copies this shape's base state (orientation, scale, offset, dynamic flag, point sampler) + * into {@code target} and returns {@code target}. Dimension-specific fields are the + * responsibility of each concrete class's own {@link #clone()} implementation. + * + * @param target the shape to copy state into; must not be null + * @return {@code target}, for chaining + */ Shape copyTo(Shape target); } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Sphere.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Sphere.java index b1b9bb6..12afaa9 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Sphere.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Sphere.java @@ -3,8 +3,8 @@ import com.sovdee.shapes.sampling.SamplingStyle; import org.joml.Vector3d; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; public class Sphere extends AbstractShape implements RadialShape { @@ -33,12 +33,11 @@ public Sphere(double radius) { // --- Static calculation methods --- - private static Set calculateFibonacciSphere(int pointCount, double radius) { - return calculateFibonacciSphere(pointCount, radius, Math.PI); + private static void calculateFibonacciSphere(List points, int pointCount, double radius) { + calculateFibonacciSphere(points, pointCount, radius, Math.PI); } - private static Set calculateFibonacciSphere(int pointCount, double radius, double angleCutoff) { - Set points = new LinkedHashSet<>(); + private static void calculateFibonacciSphere(List points, int pointCount, double radius, double angleCutoff) { double y = 1; if (angleCutoff > Math.PI) angleCutoff = Math.PI; double yLimit = Math.cos(angleCutoff); @@ -50,7 +49,7 @@ private static Set calculateFibonacciSphere(int pointCount, double rad points.add(new Vector3d(r * SPHERE_THETA_COS[i], y * radius, r * SPHERE_THETA_SIN[i])); y -= yStep; if (y <= yLimit) { - return points; + return; } } if (pointCount > preCompPoints) { @@ -60,34 +59,37 @@ private static Set calculateFibonacciSphere(int pointCount, double rad points.add(new Vector3d(r * Math.cos(theta), y * radius, r * Math.sin(theta))); y -= yStep; if (y <= yLimit) { - return points; + return; } } } - return points; } // --- Generation methods --- @Override - public void generateOutline(Set points, double density) { + public void generateOutline(List points, double density) { this.generateSurface(points, density); } @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { int pointCount = 4 * (int) (Math.PI * radius * radius / (density * density)); - points.addAll(calculateFibonacciSphere(pointCount, radius, cutoffAngle)); + if (points instanceof ArrayList arrayList) + arrayList.ensureCapacity(points.size() + pointCount); + calculateFibonacciSphere(points, pointCount, radius, cutoffAngle); } @Override - public void generateFilled(Set points, double density) { + public void generateFilled(List points, double density) { + if (points instanceof ArrayList arrayList) + arrayList.ensureCapacity(points.size() + (int) (1.333 * Math.PI * radius * radius * radius / (density * density * density))); int subSpheres = (int) (radius / density) - 1; double radiusStep = radius / subSpheres; for (int i = 1; i < subSpheres; i++) { double subRadius = i * radiusStep; int pointCount = 4 * (int) (Math.PI * subRadius * subRadius / (density * density)); - points.addAll(calculateFibonacciSphere(pointCount, subRadius, cutoffAngle)); + calculateFibonacciSphere(points, pointCount, subRadius, cutoffAngle); } } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Star.java b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Star.java index c0a262d..2f08448 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Star.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/shapes/Star.java @@ -4,8 +4,7 @@ import com.sovdee.shapes.util.VectorUtil; import org.joml.Vector3d; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.List; public class Star extends AbstractShape { @@ -20,28 +19,31 @@ public Star(double innerRadius, double outerRadius, double angle) { this.angle = Math.clamp(angle, Shape.EPSILON, Math.PI); } - private static Set calculateStar(double innerRadius, double outerRadius, double angle, double density) { - Set points = new LinkedHashSet<>(); + private static void calculateStar(List points, double innerRadius, double outerRadius, double angle, double density) { Vector3d outerVertex = new Vector3d(outerRadius, 0, 0); Vector3d innerVertex = new Vector3d(innerRadius, 0, 0); for (double theta = 0; theta < 2 * Math.PI; theta += angle) { Vector3d currentVertex = VectorUtil.rotateAroundY(new Vector3d(outerVertex), theta); - points.addAll(Line.calculateLine(currentVertex, VectorUtil.rotateAroundY(new Vector3d(innerVertex), theta + angle / 2), density)); - points.addAll(Line.calculateLine(currentVertex, VectorUtil.rotateAroundY(new Vector3d(innerVertex), theta - angle / 2), density)); + Line.calculateLine(points, currentVertex, VectorUtil.rotateAroundY(new Vector3d(innerVertex), theta + angle / 2), density); + // Second line from same vertex - skip duplicate first point + int before = points.size(); + Line.calculateLine(points, currentVertex, VectorUtil.rotateAroundY(new Vector3d(innerVertex), theta - angle / 2), density); + if (points.size() > before) { + points.remove(before); + } } - return points; } @Override - public void generateOutline(Set points, double density) { - points.addAll(calculateStar(innerRadius, outerRadius, angle, density)); + public void generateOutline(List points, double density) { + calculateStar(points, innerRadius, outerRadius, angle, density); } @Override - public void generateSurface(Set points, double density) { + public void generateSurface(List points, double density) { double minRadius = Math.min(innerRadius, outerRadius); for (double r = 0; r < minRadius; r += density) { - points.addAll(calculateStar(innerRadius - r, outerRadius - r, angle, density)); + calculateStar(points, innerRadius - r, outerRadius - r, angle, density); } } diff --git a/shapes-lib/src/main/java/com/sovdee/shapes/util/VectorUtil.java b/shapes-lib/src/main/java/com/sovdee/shapes/util/VectorUtil.java index 8519212..27ce670 100644 --- a/shapes-lib/src/main/java/com/sovdee/shapes/util/VectorUtil.java +++ b/shapes-lib/src/main/java/com/sovdee/shapes/util/VectorUtil.java @@ -3,9 +3,7 @@ import org.joml.Quaterniond; import org.joml.Vector3d; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * Helper methods for JOML Vector3d operations that mirror Bukkit Vector convenience methods. @@ -33,19 +31,17 @@ public static Vector3d rotateAroundY(Vector3d v, double angle) { /** * Transforms a list of vectors using a quaternion, modifying them in place. */ - public static List transform(Quaterniond quaternion, List vectors) { - vectors.replaceAll(quaternion::transform); - return vectors; + public static void transform(Quaterniond quaternion, List vectors) { + transform(quaternion, vectors, 0); } /** - * Transforms a set of vectors using a quaternion, returning a new set. + * Transforms vectors in a list from the given start index, modifying them in place. */ - public static Set transform(Quaterniond quaternion, Set vectors) { - Set newVectors = new HashSet<>(); - for (Vector3d vector : vectors) { - newVectors.add(quaternion.transform(new Vector3d(vector))); + public static void transform(Quaterniond quaternion, List vectors, int fromIndex) { + for (int i = fromIndex; i < vectors.size(); i++) { + quaternion.transform(vectors.get(i)); } - return newVectors; } + } diff --git a/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/ScalingModifierTest.java b/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/ScalingModifierTest.java new file mode 100644 index 0000000..3285412 --- /dev/null +++ b/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/ScalingModifierTest.java @@ -0,0 +1,126 @@ +package com.sovdee.shapes.modifiers; + +import org.joml.Vector3d; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ScalingModifierTest { + + private PointContext ctx(double x, double y, double z, List allPoints) { + ShapeBounds bounds = ShapeBounds.compute(allPoints); + PointContext c = new PointContext(); + c.bounds = bounds; + c.x = x; c.y = y; c.z = z; + return c; + } + + /** Points spanning Y=[0,1] so normalizedY works predictably. */ + private List ySpan() { + return List.of(new Vector3d(0, 0, 0), new Vector3d(0, 1, 0)); + } + + /** Points spanning X=[0,1]. */ + private List xSpan() { + return List.of(new Vector3d(0, 0, 0), new Vector3d(1, 0, 0)); + } + + @Test + void scalesAtInputMin() { + ScalingModifier mod = new ScalingModifier(0.5, 2.0, StandardInput.Y, ScaleAxes.XZ); + PointContext c = ctx(1, 0, 1, ySpan()); + mod.modify(c); + assertEquals(0.5, c.x, 1e-9, "At normalizedY=0, scale=startScale=0.5"); + assertEquals(0.5, c.z, 1e-9); + assertEquals(0.0, c.y, 1e-9, "Y should be unchanged"); + } + + @Test + void scalesAtInputMax() { + ScalingModifier mod = new ScalingModifier(0.5, 2.0, StandardInput.Y, ScaleAxes.XZ); + PointContext c = ctx(1, 1, 1, ySpan()); + mod.modify(c); + assertEquals(2.0, c.x, 1e-9, "At normalizedY=1, scale=endScale=2.0"); + assertEquals(2.0, c.z, 1e-9); + } + + @Test + void scalesAtMidpoint() { + ScalingModifier mod = new ScalingModifier(0.0, 2.0, StandardInput.Y, ScaleAxes.XZ); + PointContext c = ctx(1, 0.5, 1, ySpan()); + mod.modify(c); + double expected = 0.0 + 0.5 * (2.0 - 0.0); // lerp(0, 2, 0.5) = 1 + assertEquals(expected, c.x, 1e-9); + assertEquals(expected, c.z, 1e-9); + } + + @Test + void xzAxes_yUnchanged() { + ScalingModifier mod = new ScalingModifier(1.0, 3.0, StandardInput.Y, ScaleAxes.XZ); + PointContext c = ctx(2, 0.5, -3, ySpan()); + double origY = c.y; + mod.modify(c); + assertEquals(origY, c.y, 1e-9, "Y coordinate must not be changed when scaling XZ"); + } + + @Test + void xzAxes_drivenByX() { + ScalingModifier mod = new ScalingModifier(0.0, 2.0, StandardInput.X, ScaleAxes.XZ); + PointContext c = ctx(1, 1, 1, xSpan()); // normalizedX=1 → scale=2.0 + mod.modify(c); + assertEquals(2.0, c.x, 1e-9, "X scaled by endScale=2.0 at normalizedX=1"); + assertEquals(2.0, c.z, 1e-9, "Z scaled by endScale=2.0"); + assertEquals(1.0, c.y, 1e-9, "Y unchanged when scaling XZ"); + } + + @Test + void xyzAxes_allScaled() { + ScalingModifier mod = new ScalingModifier(2.0, 2.0, StandardInput.Y, ScaleAxes.XYZ); + PointContext c = ctx(1, 0.5, 1, ySpan()); + mod.modify(c); + assertEquals(2.0, c.x, 1e-9); + assertEquals(1.0, c.y, 1e-9, "Y also scaled at 0.5 position"); + assertEquals(2.0, c.z, 1e-9); + } + + @Test + void yzAxes_xUnchanged() { + ScalingModifier mod = new ScalingModifier(2.0, 2.0, StandardInput.Y, ScaleAxes.YZ); + PointContext c = ctx(3, 0.5, 1, ySpan()); + mod.modify(c); + assertEquals(3.0, c.x, 1e-9, "X unchanged when scaling YZ"); + } + + @Test + void modifierHashChangesWithParams() { + ScalingModifier a = new ScalingModifier(1.0, 2.0, StandardInput.Y, ScaleAxes.XZ); + ScalingModifier b = new ScalingModifier(1.5, 2.0, StandardInput.Y, ScaleAxes.XZ); + assertNotEquals(a.modifierHash(), b.modifierHash(), "Different startScale"); + } + + @Test + void modifierHashChangesWithInput() { + ScalingModifier y = new ScalingModifier(1.0, 2.0, StandardInput.Y, ScaleAxes.XZ); + ScalingModifier x = new ScalingModifier(1.0, 2.0, StandardInput.X, ScaleAxes.XZ); + ScalingModifier t = new ScalingModifier(1.0, 2.0, StandardInput.T, ScaleAxes.XZ); + assertNotEquals(y.modifierHash(), x.modifierHash(), "Y vs X input"); + assertNotEquals(y.modifierHash(), t.modifierHash(), "Y vs T input"); + } + + @Test + void modifierHashChangesWithAxes() { + ScalingModifier xz = new ScalingModifier(1.0, 2.0, StandardInput.Y, ScaleAxes.XZ); + ScalingModifier xyz = new ScalingModifier(1.0, 2.0, StandardInput.Y, ScaleAxes.XYZ); + assertNotEquals(xz.modifierHash(), xyz.modifierHash(), "XZ vs XYZ axes"); + } + + @Test + void cloneIsIndependent() { + ScalingModifier original = new ScalingModifier(1.0, 2.0, StandardInput.Y, ScaleAxes.XZ); + ScalingModifier copy = original.clone(); + copy.setStartScale(99.0); + assertEquals(1.0, original.getStartScale(), "Mutating clone must not affect original"); + } +} diff --git a/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/ShapeBoundsTest.java b/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/ShapeBoundsTest.java new file mode 100644 index 0000000..bad4a7e --- /dev/null +++ b/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/ShapeBoundsTest.java @@ -0,0 +1,120 @@ +package com.sovdee.shapes.modifiers; + +import org.joml.Vector3d; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ShapeBoundsTest { + + @Test + void computeEmpty() { + ShapeBounds b = ShapeBounds.compute(List.of()); + assertEquals(0.0, b.minX()); + assertEquals(0.0, b.maxX()); + assertEquals(0.0, b.invRangeX()); + assertEquals(0.0, b.invRangeY()); + assertEquals(0.0, b.invRangeZ()); + assertEquals(0.0, b.maxRadiusXZ()); + } + + @Test + void computeSinglePoint() { + ShapeBounds b = ShapeBounds.compute(List.of(new Vector3d(3, 5, 7))); + assertEquals(3.0, b.minX()); + assertEquals(3.0, b.maxX()); + assertEquals(0.0, b.invRangeX(), "invRangeX should be 0 for zero-width extent"); + assertEquals(0.0, b.invRangeY()); + assertEquals(0.0, b.invRangeZ()); + } + + @Test + void computeMultiplePoints() { + List pts = List.of( + new Vector3d(-1, 0, 2), + new Vector3d(3, 4, -2), + new Vector3d(0, 2, 0) + ); + ShapeBounds b = ShapeBounds.compute(pts); + assertEquals(-1.0, b.minX()); + assertEquals(3.0, b.maxX()); + assertEquals(0.0, b.minY()); + assertEquals(4.0, b.maxY()); + assertEquals(-2.0, b.minZ()); + assertEquals(2.0, b.maxZ()); + assertEquals(4.0, b.width()); + assertEquals(4.0, b.height()); + assertEquals(4.0, b.length()); + } + + @Test + void normalizeXYZ() { + List pts = List.of(new Vector3d(0, 0, 0), new Vector3d(2, 4, 6)); + ShapeBounds b = ShapeBounds.compute(pts); + + assertEquals(0.0, b.normalizeX(0.0), 1e-9); + assertEquals(1.0, b.normalizeX(2.0), 1e-9); + assertEquals(0.5, b.normalizeX(1.0), 1e-9); + + assertEquals(0.5, b.normalizeY(2.0), 1e-9); + assertEquals(0.5, b.normalizeZ(3.0), 1e-9); + } + + @Test + void normalizeRadial() { + // Points on a unit circle in XZ + List pts = List.of( + new Vector3d(1, 0, 0), + new Vector3d(-1, 0, 0), + new Vector3d(0, 0, 1), + new Vector3d(0, 0, -1) + ); + ShapeBounds b = ShapeBounds.compute(pts); + assertEquals(1.0, b.maxRadiusXZ(), 1e-9); + // Point on the unit circle → normalized radial = 1 + assertEquals(1.0, b.normalizeRadial(1, 0), 1e-9); + // Origin → normalized radial = 0 + assertEquals(0.0, b.normalizeRadial(0, 0), 1e-9); + // Point at (1/√2, 1/√2) in XZ: radius = sqrt(0.5 + 0.5) = 1.0 → normalizes to 1.0 + assertEquals(1.0, b.normalizeRadial(1.0 / Math.sqrt(2), 1.0 / Math.sqrt(2)), 1e-6); + // Point at half-radius on X axis + assertEquals(0.5, b.normalizeRadial(0.5, 0), 1e-9); + } + + @Test + void zeroDimensionNormalization() { + // Flat shape — all points at Y=5 + List pts = List.of( + new Vector3d(0, 5, 0), + new Vector3d(1, 5, 0), + new Vector3d(0, 5, 1) + ); + ShapeBounds b = ShapeBounds.compute(pts); + assertEquals(0.0, b.invRangeY(), "Flat shape should have invRangeY=0"); + // normalizeY should return 0 (no divide-by-zero) + assertEquals(0.0, b.normalizeY(5.0), 1e-9); + assertEquals(0.0, b.normalizeY(0.0), 1e-9); + } + + @Test + void maxRadiusXZ_ignoresY() { + // (1, 100, 0) → XZ radius = 1; (0, -100, 1) → XZ radius = 1 + // A huge Y value does not contribute to the max XZ radius + List pts = List.of( + new Vector3d(1, 100, 0), + new Vector3d(0, -100, 1) + ); + ShapeBounds b = ShapeBounds.compute(pts); + assertEquals(1.0, b.maxRadiusXZ(), 1e-9, "Max XZ radius should only use X and Z components"); + + // Additional check: a point with larger XZ distance should increase maxRadiusXZ + List pts2 = List.of( + new Vector3d(3, 0, 4), // XZ radius = 5 + new Vector3d(0, 1000, 0) // XZ radius = 0 + ); + ShapeBounds b2 = ShapeBounds.compute(pts2); + assertEquals(5.0, b2.maxRadiusXZ(), 1e-9); + } +} diff --git a/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/TwistModifierTest.java b/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/TwistModifierTest.java new file mode 100644 index 0000000..20f8a7c --- /dev/null +++ b/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/TwistModifierTest.java @@ -0,0 +1,137 @@ +package com.sovdee.shapes.modifiers; + +import org.joml.Vector3d; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class TwistModifierTest { + + private PointContext ctx(double x, double y, double z, List allPoints) { + ShapeBounds bounds = ShapeBounds.compute(allPoints); + PointContext c = new PointContext(); + c.bounds = bounds; + c.x = x; c.y = y; c.z = z; + return c; + } + + private List ySpan() { + return List.of(new Vector3d(0, 0, 0), new Vector3d(0, 1, 0)); + } + + private List xSpan() { + return List.of(new Vector3d(0, 0, 0), new Vector3d(1, 0, 0)); + } + + // --- Default Y-input, XZ-plane tests --- + + @Test + void noTwistAtBottom() { + TwistModifier mod = new TwistModifier(Math.PI); + PointContext c = ctx(1, 0, 0, ySpan()); + mod.modify(c); + // angle = PI * normalizedY(0) = PI * 0 = 0 → identity rotation + assertEquals(1.0, c.x, 1e-9); + assertEquals(0.0, c.z, 1e-9); + } + + @Test + void fullTwistAtTop() { + // totalAngle = 2π, normalizedY=1 → full 360° rotation → identity + TwistModifier mod = new TwistModifier(2 * Math.PI); + PointContext c = ctx(1, 1, 0, ySpan()); + mod.modify(c); + assertEquals(1.0, c.x, 1e-9, "Full 2π twist should return to original X"); + assertEquals(0.0, c.z, 1e-9, "Full 2π twist should return to original Z"); + } + + @Test + void quarterTwistAtMidpoint() { + // totalAngle=π, normalizedY=0.5 → angle=π/2 (90°) + // newX = x*cos - z*sin = 1*cos(π/2) - 0*sin(π/2) = 0 + // newZ = x*sin + z*cos = 1*sin(π/2) + 0*cos(π/2) = 1 + TwistModifier mod = new TwistModifier(Math.PI); + PointContext c = ctx(1, 0.5, 0, ySpan()); + mod.modify(c); + assertEquals(0.0, c.x, 1e-9); + assertEquals(1.0, c.z, 1e-9); + } + + @Test + void yCoordinateUnchanged() { + TwistModifier mod = new TwistModifier(Math.PI); + PointContext c = ctx(1, 0.5, 0, ySpan()); + double origY = c.y; + mod.modify(c); + assertEquals(origY, c.y, 1e-9, "XZ-plane twist must not change Y"); + } + + // --- X-input, YZ-plane twist: rotates YZ plane, X unchanged --- + + @Test + void xInput_yzPlane_quarterTwist() { + // totalAngle=π, normalizedX=0.5 → angle=π/2 + // point (0, 1, 0): newY = 1*cos(π/2) - 0*sin(π/2) = 0, newZ = 1*sin(π/2) + 0*cos(π/2) = 1 + TwistModifier mod = new TwistModifier(Math.PI, StandardInput.X, RotationPlane.YZ); + PointContext c = ctx(0.5, 1, 0, xSpan()); + mod.modify(c); + assertEquals(0.0, c.y, 1e-9, "Y rotated to 0"); + assertEquals(1.0, c.z, 1e-9, "Z rotated to 1"); + assertEquals(0.5, c.x, 1e-9, "X must not change in YZ-plane twist"); + } + + @Test + void xInput_xCoordinateUnchanged() { + TwistModifier mod = new TwistModifier(Math.PI, StandardInput.X, RotationPlane.YZ); + PointContext c = ctx(0.5, 1, 0, xSpan()); + double origX = c.x; + mod.modify(c); + assertEquals(origX, c.x, 1e-9, "YZ-plane twist must not change X"); + } + + // --- XY-plane twist --- + + @Test + void xyPlane_zCoordinateUnchanged() { + TwistModifier mod = new TwistModifier(Math.PI, StandardInput.Y, RotationPlane.XY); + PointContext c = ctx(1, 0.5, 7, ySpan()); + double origZ = c.z; + mod.modify(c); + assertEquals(origZ, c.z, 1e-9, "XY-plane twist must not change Z"); + } + + // --- Hash and clone --- + + @Test + void modifierHashChangesWithAngle() { + TwistModifier a = new TwistModifier(Math.PI); + TwistModifier b = new TwistModifier(Math.PI / 2); + assertNotEquals(a.modifierHash(), b.modifierHash()); + } + + @Test + void modifierHashChangesWithInput() { + TwistModifier y = new TwistModifier(Math.PI, StandardInput.Y, RotationPlane.XZ); + TwistModifier x = new TwistModifier(Math.PI, StandardInput.X, RotationPlane.XZ); + TwistModifier t = new TwistModifier(Math.PI, StandardInput.T, RotationPlane.XZ); + assertNotEquals(y.modifierHash(), x.modifierHash(), "Y vs X input"); + assertNotEquals(y.modifierHash(), t.modifierHash(), "Y vs T input"); + } + + @Test + void modifierHashChangesWithPlane() { + TwistModifier xz = new TwistModifier(Math.PI, StandardInput.Y, RotationPlane.XZ); + TwistModifier xy = new TwistModifier(Math.PI, StandardInput.Y, RotationPlane.XY); + assertNotEquals(xz.modifierHash(), xy.modifierHash(), "XZ vs XY plane"); + } + + @Test + void cloneIsIndependent() { + TwistModifier original = new TwistModifier(Math.PI); + TwistModifier copy = original.clone(); + copy.setTotalAngle(99.0); + assertEquals(Math.PI, original.getTotalAngle(), "Mutating clone must not affect original"); + } +} diff --git a/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/WaveModifierTest.java b/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/WaveModifierTest.java new file mode 100644 index 0000000..be43666 --- /dev/null +++ b/shapes-lib/src/test/java/com/sovdee/shapes/modifiers/WaveModifierTest.java @@ -0,0 +1,138 @@ +package com.sovdee.shapes.modifiers; + +import org.joml.Vector3d; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class WaveModifierTest { + + private PointContext ctxWithIndex(double x, double y, double z, int index, int total) { + ShapeBounds bounds = ShapeBounds.compute(List.of(new Vector3d(x, y, z))); + PointContext c = new PointContext(); + c.bounds = bounds; + c.x = x; c.y = y; c.z = z; + c.index = index; + c.totalPoints = total; + return c; + } + + private PointContext ctxT(int index, int total) { + return ctxWithIndex(0, 0, 0, index, total); + } + + // frequency=1 means one full sine cycle over T=[0,1]. + // At T=0.25 with freq=1: sin(2π * 1 * 0.25) = sin(π/2) = 1 + + @Test + void displaceY_byT_quarterCycle() { + // T=0.25, freq=1 → sin(2π * 0.25) = sin(π/2) = 1 → displacement = amplitude + WaveModifier mod = new WaveModifier(1.0, 1.0, 0.0, StandardInput.T, SpatialAxis.Y); + PointContext c = ctxT(5, 20); // T = 5/20 = 0.25 + mod.modify(c); + assertEquals(Math.sin(2 * Math.PI * 0.25), c.y, 1e-9); + } + + @Test + void displaceY_byT_halfCycle() { + // T=0.5, freq=1 → sin(2π * 0.5) = sin(π) ≈ 0 + WaveModifier mod = new WaveModifier(1.0, 1.0, 0.0, StandardInput.T, SpatialAxis.Y); + PointContext c = ctxT(10, 20); // T = 0.5 + mod.modify(c); + assertEquals(Math.sin(2 * Math.PI * 0.5), c.y, 1e-9); + } + + @Test + void displaceY_byT_twoCycles() { + // T=0.25, freq=2 → sin(2π * 2 * 0.25) = sin(π) ≈ 0 + WaveModifier mod = new WaveModifier(1.0, 2.0, 0.0, StandardInput.T, SpatialAxis.Y); + PointContext c = ctxT(5, 20); // T = 0.25 + mod.modify(c); + assertEquals(Math.sin(2 * Math.PI * 2 * 0.25), c.y, 1e-9); + } + + @Test + void displaceByT_zeroTotalPoints() { + WaveModifier mod = new WaveModifier(1.0, 1.0, 0.0, StandardInput.T, SpatialAxis.Y); + PointContext c = ctxT(0, 0); // totalPoints=0, T=0 → sin(0) = 0 + assertDoesNotThrow(() -> mod.modify(c)); + assertEquals(0.0, c.y, 1e-9, "T=0 when totalPoints=0 → zero displacement"); + } + + @Test + void zeroAmplitude() { + WaveModifier mod = new WaveModifier(0.0, 3.0, 0.0, StandardInput.T, SpatialAxis.Y); + PointContext c = ctxT(5, 20); + double origY = c.y; + mod.modify(c); + assertEquals(origY, c.y, 1e-9, "Zero amplitude → no displacement"); + } + + @Test + void zeroFrequency_withPhase() { + // freq=0: sin(0 * T + phase) = sin(phase); T is irrelevant + double phase = Math.PI / 6; // sin(PI/6) = 0.5 + WaveModifier mod = new WaveModifier(2.0, 0.0, phase, StandardInput.T, SpatialAxis.Y); + PointContext c = ctxT(15, 20); // T = 0.75, irrelevant since freq=0 + mod.modify(c); + assertEquals(2.0 * Math.sin(phase), c.y, 1e-9); + } + + @Test + void phaseShift_producesDistinctDisplacement() { + WaveModifier mod1 = new WaveModifier(1.0, 1.0, 0.0, StandardInput.T, SpatialAxis.Y); + WaveModifier mod2 = new WaveModifier(1.0, 1.0, Math.PI / 4, StandardInput.T, SpatialAxis.Y); + PointContext c1 = ctxT(5, 20); + PointContext c2 = ctxT(5, 20); + mod1.modify(c1); + mod2.modify(c2); + assertNotEquals(c1.y, c2.y, "Different phase → different displacement"); + } + + @ParameterizedTest + @EnumSource(SpatialAxis.class) + void allOutputAxes_nonOutputAxesUnchanged(SpatialAxis outputAxis) { + WaveModifier mod = new WaveModifier(1.0, 1.0, 0.0, StandardInput.T, outputAxis); + PointContext c = ctxT(5, 20); // T=0.25 → non-zero displacement + double origX = c.x, origY = c.y, origZ = c.z; + mod.modify(c); + if (outputAxis != SpatialAxis.X) assertEquals(origX, c.x, 1e-9, "X unchanged when output != X"); + if (outputAxis != SpatialAxis.Y) assertEquals(origY, c.y, 1e-9, "Y unchanged when output != Y"); + if (outputAxis != SpatialAxis.Z) assertEquals(origZ, c.z, 1e-9, "Z unchanged when output != Z"); + } + + @Test + void modifierHashCoversAllParams() { + WaveModifier base = new WaveModifier(1.0, 2.0, 0.5, StandardInput.X, SpatialAxis.Y); + assertNotEquals(base.modifierHash(), + new WaveModifier(9.0, 2.0, 0.5, StandardInput.X, SpatialAxis.Y).modifierHash(), + "Different amplitude"); + assertNotEquals(base.modifierHash(), + new WaveModifier(1.0, 9.0, 0.5, StandardInput.X, SpatialAxis.Y).modifierHash(), + "Different frequency"); + assertNotEquals(base.modifierHash(), + new WaveModifier(1.0, 2.0, 9.0, StandardInput.X, SpatialAxis.Y).modifierHash(), + "Different phase"); + assertNotEquals(base.modifierHash(), + new WaveModifier(1.0, 2.0, 0.5, StandardInput.Z, SpatialAxis.Y).modifierHash(), + "Different input"); + assertNotEquals(base.modifierHash(), + new WaveModifier(1.0, 2.0, 0.5, StandardInput.X, SpatialAxis.Z).modifierHash(), + "Different output axis"); + assertNotEquals(base.modifierHash(), + new WaveModifier(1.0, 2.0, 0.5, StandardInput.T, SpatialAxis.Y).modifierHash(), + "T input differs from X"); + } + + @Test + void cloneIsIndependent() { + WaveModifier original = new WaveModifier(1.0, 2.0, 0.0, StandardInput.X, SpatialAxis.Y); + WaveModifier copy = original.clone(); + copy.setAmplitude(99.0); + assertEquals(1.0, original.getAmplitude(), "Mutating clone must not affect original"); + } +} diff --git a/shapes-lib/src/test/java/com/sovdee/shapes/sampling/DefaultPointSamplerTest.java b/shapes-lib/src/test/java/com/sovdee/shapes/sampling/DefaultPointSamplerTest.java new file mode 100644 index 0000000..961ce31 --- /dev/null +++ b/shapes-lib/src/test/java/com/sovdee/shapes/sampling/DefaultPointSamplerTest.java @@ -0,0 +1,340 @@ +package com.sovdee.shapes.sampling; + +import com.sovdee.shapes.modifiers.PointContext; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.shapes.modifiers.ShapeBounds; +import com.sovdee.shapes.modifiers.ScalingModifier; +import com.sovdee.shapes.modifiers.ScaleAxes; +import com.sovdee.shapes.modifiers.StandardInput; +import com.sovdee.shapes.shapes.Circle; +import org.joml.Quaterniond; +import org.joml.Vector3d; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class DefaultPointSamplerTest { + + // ----- Minimal ShapeRenderer helpers ----- + + static class CountingRenderer implements ShapeRenderer { + int beginCount, endCount, renderCount; + int lastBeginTotal; + final PointContext ctx = new PointContext(); + + @Override public void begin(int total) { beginCount++; lastBeginTotal = total; } + @Override public void renderPoint(PointContext p) { renderCount++; } + @Override public PointContext getContext() { return ctx; } + @Override public void end() { endCount++; } + } + + /** A render modifier (not PreRender) that counts prepare() and modify() calls. */ + static class CountingRenderModifier implements PointModifier { + int prepareCount, modifyCount; + + @Override public void prepare(ShapeBounds b) { prepareCount++; } + @Override public void modify(PointContext p) { modifyCount++; } + @Override public int modifierHash() { return 42; } + @Override public PointModifier clone() { + try { return (CountingRenderModifier) super.clone(); } + catch (CloneNotSupportedException e) { throw new RuntimeException(e); } + } + } + + /** A geometry modifier (PreRender) that counts calls. */ + static class CountingGeoModifier extends PointModifier.PreRenderPointModifier { + int modifyCount; + @Override public void modify(PointContext p) { modifyCount++; } + @Override public int modifierHash() { return 7; } + } + + // A PointContext subclass for context-type validation tests + static class SubContext extends PointContext { + int extra; + @Override public void reset() { extra = 0; } + } + + static class SubContextModifier implements PointModifier { + @Override public void modify(SubContext p) { p.extra = 99; } + @Override public int modifierHash() { return 1; } + @Override public Class contextType() { return SubContext.class; } + @Override public PointModifier clone() { + try { return (SubContextModifier) super.clone(); } + catch (CloneNotSupportedException e) { throw new RuntimeException(e); } + } + } + + static class SubContextRenderer implements ShapeRenderer { + final SubContext ctx = new SubContext(); + int renderCount; + @Override public void begin(int total) {} + @Override public void renderPoint(SubContext p) { renderCount++; } + @Override public SubContext getContext() { return ctx; } + @Override public void end() {} + } + + // ----- Test setup ----- + + Circle circle; + DefaultPointSampler sampler; + + @BeforeEach + void setUp() { + circle = new Circle(1.0); + sampler = new DefaultPointSampler(); + sampler.setDensity(0.5); // coarse for speed + } + + // ----- Caching tests ----- + + @Test + void cacheHit_sameState() { + List first = sampler.getPoints(circle); + List second = sampler.getPoints(circle); + assertSame(first, second, "Same state should return cached list"); + } + + @Test + void cacheMiss_onDensityChange() { + List first = sampler.getPoints(circle); + sampler.setDensity(0.2); + List second = sampler.getPoints(circle); + assertNotSame(first, second, "Density change should invalidate cache"); + } + + @Test + void cacheMiss_onStyleChange() { + List first = sampler.getPoints(circle); + sampler.setStyle(SamplingStyle.SURFACE); + List second = sampler.getPoints(circle); + assertNotSame(first, second, "Style change should invalidate cache"); + } + + @Test + void cacheMiss_onShapeInvalidate() { + List first = sampler.getPoints(circle); + circle.setRadius(2.0); // bumps version + List second = sampler.getPoints(circle); + assertNotSame(first, second, "Shape geometry change should invalidate cache"); + } + + @Test + void cacheHit_renderModifierAdded() { + List first = sampler.getPoints(circle); + // Render modifiers don't affect geometry; cache should survive + sampler.addModifier(new CountingRenderModifier()); + List second = sampler.getPoints(circle); + assertSame(first, second, "Adding a render modifier must not invalidate geometry cache"); + } + + @Test + void cacheMiss_geoModifierAdded() { + List first = sampler.getPoints(circle); + sampler.addModifier(new CountingGeoModifier()); // PreRenderPointModifier → calls markDirty + List second = sampler.getPoints(circle); + assertNotSame(first, second, "Adding a geo modifier must invalidate cache"); + } + + // ----- Geometry modifier dispatch ----- + + @Test + void geoModifier_appliedToPoints() { + // Scale from 0 to 0 collapses all XZ to zero + sampler.addModifier(new ScalingModifier(0.0, 0.0, StandardInput.Y, ScaleAxes.XZ)); + List points = sampler.getPoints(circle); + for (Vector3d p : points) { + assertEquals(0.0, p.x, 1e-9, "ScalingModifier(0,0) should zero all X"); + assertEquals(0.0, p.z, 1e-9, "ScalingModifier(0,0) should zero all Z"); + } + } + + @Test + void geoModifier_notCalledDuringRender() { + CountingGeoModifier geoMod = new CountingGeoModifier(); + sampler.addModifier(geoMod); + + int pointCount = sampler.getPoints(circle).size(); + int afterGetPoints = geoMod.modifyCount; + assertEquals(pointCount, afterGetPoints, "Geo modifier should be called once per point in getPoints()"); + + // render() should NOT re-invoke the geo modifier + CountingRenderer renderer = new CountingRenderer(); + sampler.render(circle, circle.getOrientation(), renderer); + assertEquals(afterGetPoints, geoMod.modifyCount, "Geo modifier must not run again during render()"); + } + + // ----- Render loop ----- + + @Test + void render_callsBeginAndEnd() { + CountingRenderer renderer = new CountingRenderer(); + sampler.render(circle, circle.getOrientation(), renderer); + assertEquals(1, renderer.beginCount); + assertEquals(1, renderer.endCount); + } + + @Test + void render_callsRenderPointForEachPoint() { + CountingRenderer renderer = new CountingRenderer(); + sampler.render(circle, circle.getOrientation(), renderer); + int pointCount = sampler.getPoints(circle).size(); + assertEquals(pointCount, renderer.renderCount, + "renderPoint() should be called exactly once per point"); + } + + @Test + void render_preparesModifier() { + CountingRenderModifier renderMod = new CountingRenderModifier(); + sampler.addModifier(renderMod); + CountingRenderer renderer = new CountingRenderer(); + + sampler.render(circle, circle.getOrientation(), renderer); + assertEquals(1, renderMod.prepareCount, "Render modifier prepare() must be called once per render()"); + + sampler.render(circle, circle.getOrientation(), renderer); + assertEquals(2, renderMod.prepareCount, "Prepare must be called once per render() invocation"); + } + + // ----- Context type validation ----- + + @Test + void render_rejectsIncompatibleModifier() { + // SubContextModifier requires SubContext, but renderer provides plain PointContext + sampler.addModifier(new SubContextModifier()); + CountingRenderer renderer = new CountingRenderer(); // provides PointContext + assertThrows(IllegalStateException.class, + () -> sampler.render(circle, circle.getOrientation(), renderer), + "Should throw when modifier context type is not assignable from renderer context type"); + } + + @Test + void render_acceptsCompatibleModifier() { + sampler.addModifier(new CountingRenderModifier()); // contextType=PointContext + CountingRenderer renderer = new CountingRenderer(); // provides PointContext + assertDoesNotThrow(() -> sampler.render(circle, circle.getOrientation(), renderer)); + } + + @Test + void render_acceptsSubtypeContext() { + // Modifier needs PointContext; renderer provides SubContext (which IS a PointContext) + sampler.addModifier(new CountingRenderModifier()); // contextType=PointContext + SubContextRenderer renderer = new SubContextRenderer(); // provides SubContext + assertDoesNotThrow(() -> sampler.render(circle, circle.getOrientation(), renderer)); + } + + // ----- Transform pipeline ----- + + @Test + void getPoints_appliesScale() { + circle.setScale(2.0); + List points = sampler.getPoints(circle, new Quaterniond()); + for (Vector3d p : points) { + double dist = Math.sqrt(p.x * p.x + p.z * p.z); + assertEquals(2.0, dist, 1e-6, "Scale=2 on radius=1 circle → points at distance 2"); + } + } + + @Test + void getPoints_appliesOffset() { + double offsetX = 5.0; + circle.setOffset(new Vector3d(offsetX, 0, 0)); + + // Get points without offset for comparison + Circle baseCircle = new Circle(1.0); + DefaultPointSampler baseSampler = new DefaultPointSampler(); + baseSampler.setDensity(0.5); + List base = new ArrayList<>(baseSampler.getPoints(baseCircle, new Quaterniond())); + + List offset = sampler.getPoints(circle, new Quaterniond()); + assertEquals(base.size(), offset.size()); + for (int i = 0; i < base.size(); i++) { + assertEquals(base.get(i).x + offsetX, offset.get(i).x, 1e-6, + "Offset X should shift all points"); + } + } + + // ----- Auto-density (max-points) ----- + + @Test + void autoDensity_capsPointCount() { + // Fresh sampler — no explicit density. Large circle should be capped. + Circle large = new Circle(10.0); + DefaultPointSampler fresh = new DefaultPointSampler(); + // Default cap is 10 000; default density (0.25) on radius-10 circle = ~628 points — well under cap. + // Use a tiny cap so the test doesn't depend on default density math. + fresh.setMaxPoints(100); + List points = fresh.getPoints(large); + // Allow a 5% overshoot: computeDensity is an approximation and loop arithmetic + // can produce 1-2 extra points due to floating-point step accumulation. + assertTrue(points.size() <= 105, + "Auto-density should cap point count near maxPoints (≤105), got " + points.size()); + } + + @Test + void autoDensity_defaultCapIsApplied() { + // A very large circle with default sampler (no explicit density, default cap = 10 000). + Circle huge = new Circle(1000.0); + DefaultPointSampler fresh = new DefaultPointSampler(); + assertFalse(fresh.isDensityExplicit()); + List points = fresh.getPoints(huge); + assertTrue(points.size() <= 10_500, + "Default 10 000 cap should be respected (≤10 500 allowing FP overshoot), got " + points.size()); + } + + @Test + void explicitDensity_overridesAutoCap() { + Circle large = new Circle(10.0); + DefaultPointSampler s = new DefaultPointSampler(); + s.setMaxPoints(50); // very tight auto cap + s.setDensity(0.5); // explicit density + assertTrue(s.isDensityExplicit()); + + List points = s.getPoints(large); + // With radius 10 and density 0.5, we expect ~125 points (2π*10 / (1/0.5)). + // The auto cap of 50 must NOT be applied. + assertTrue(points.size() > 50, + "Explicit density must not be limited by maxPoints, got " + points.size()); + } + + @Test + void setMaxPoints_resetsToAutoMode() { + Circle large = new Circle(10.0); + DefaultPointSampler s = new DefaultPointSampler(); + s.setDensity(0.5); // explicit + assertTrue(s.isDensityExplicit()); + + s.setMaxPoints(50); // resets to auto + assertFalse(s.isDensityExplicit(), "setMaxPoints should reset densityExplicit to false"); + + List points = s.getPoints(large); + assertTrue(points.size() <= 53, + "After setMaxPoints auto-mode should be re-enabled, got " + points.size()); + } + + @Test + void setMaxPoints_clampedToOne() { + DefaultPointSampler s = new DefaultPointSampler(); + s.setMaxPoints(0); + assertEquals(1, s.getMaxPoints(), "setMaxPoints(0) should clamp to 1"); + s.setMaxPoints(-5); + assertEquals(1, s.getMaxPoints(), "setMaxPoints(-5) should clamp to 1"); + } + + @Test + void autoDensity_cacheInvalidatesOnShapeChange() { + Circle large = new Circle(10.0); + DefaultPointSampler s = new DefaultPointSampler(); + s.setMaxPoints(200); + + List first = s.getPoints(large); + large.setRadius(20.0); // geometry change + List second = s.getPoints(large); + assertNotSame(first, second, "Cache must invalidate when shape geometry changes in auto-density mode"); + assertTrue(second.size() <= 210, + "After geometry change auto-density should still cap near maxPoints, got " + second.size()); + } +} diff --git a/shapes-lib/src/test/java/com/sovdee/shapes/shapes/CircleTest.java b/shapes-lib/src/test/java/com/sovdee/shapes/shapes/CircleTest.java new file mode 100644 index 0000000..46110c4 --- /dev/null +++ b/shapes-lib/src/test/java/com/sovdee/shapes/shapes/CircleTest.java @@ -0,0 +1,84 @@ +package com.sovdee.shapes.shapes; + +import org.joml.Vector3d; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class CircleTest { + + private static final double DENSITY = 0.25; + + @Test + void calculateCircle_pointsOnRadius() { + List points = new ArrayList<>(); + Circle.calculateCircle(points, 1.0, DENSITY, 2 * Math.PI); + assertFalse(points.isEmpty()); + for (Vector3d p : points) { + double dist = Math.sqrt(p.x * p.x + p.z * p.z); + assertEquals(1.0, dist, 1e-9, "All outline points should lie on the circle radius"); + } + } + + @Test + void calculateCircle_pointCount() { + double radius = 2.0; + List points = new ArrayList<>(); + Circle.calculateCircle(points, radius, DENSITY, 2 * Math.PI); + double expected = (2 * Math.PI) / (DENSITY / radius); + // Allow ±5% tolerance due to floating-point step counting + assertEquals(expected, points.size(), expected * 0.05 + 2); + } + + @Test + void calculateCircle_partialCutoff() { + List full = new ArrayList<>(); + List half = new ArrayList<>(); + Circle.calculateCircle(full, 1.0, DENSITY, 2 * Math.PI); + Circle.calculateCircle(half, 1.0, DENSITY, Math.PI); + // Half cutoff should produce approximately half the points + assertEquals(full.size(), half.size() * 2, (double) full.size() * 0.1 + 2); + } + + @Test + void calculateCircle_yIsZero() { + List points = new ArrayList<>(); + Circle.calculateCircle(points, 1.0, DENSITY, 2 * Math.PI); + for (Vector3d p : points) { + assertEquals(0.0, p.y, 1e-9, "All circle outline points should have Y=0"); + } + } + + @Test + void contains_insideRadius() { + Circle c = new Circle(2.0); + assertTrue(c.contains(new Vector3d(0.5, 0, 0))); + assertTrue(c.contains(new Vector3d(0, 0, 1.9))); + } + + @Test + void contains_outsideRadius() { + Circle c = new Circle(1.0); + assertFalse(c.contains(new Vector3d(2.0, 0, 0))); + assertFalse(c.contains(new Vector3d(1.0, 0, 1.0))); + } + + @Test + void contains_withHeight() { + Circle c = new Circle(1.0, 2.0); + assertTrue(c.contains(new Vector3d(0, 1, 0)), "Inside cylinder vertically"); + assertFalse(c.contains(new Vector3d(0, 3, 0)), "Above top of cylinder"); + assertFalse(c.contains(new Vector3d(0, -0.1, 0)), "Below bottom of cylinder"); + } + + @Test + void setRadius_invalidatesVersion() { + Circle c = new Circle(1.0); + long v0 = c.getVersion(); + c.setRadius(2.0); + assertTrue(c.getVersion() > v0, "setRadius() should increment the version"); + } +} diff --git a/skript-particle/build.gradle b/skript-particle/build.gradle index 4c7c539..79de042 100644 --- a/skript-particle/build.gradle +++ b/skript-particle/build.gradle @@ -3,6 +3,7 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { id 'java' id 'com.gradleup.shadow' version '8.3.5' + id 'io.papermc.paperweight.userdev' version '2.0.0-beta.19' } group 'com.sovdee' @@ -12,6 +13,7 @@ configurations.configureEach { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.papermc.io/repository/maven-public/' @@ -23,8 +25,8 @@ repositories { dependencies { implementation project(':shapes-lib') - compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") - compileOnly("com.github.SkriptLang:Skript:2.14.0-pre1") + paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT") + compileOnly("com.github.SkriptLang:Skript:2.14.2") compileOnly "org.joml:joml:${jomlVersion}" } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/SkriptParticle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/SkriptParticle.java index 3e54386..dba6efd 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/SkriptParticle.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/SkriptParticle.java @@ -4,14 +4,24 @@ import ch.njol.skript.SkriptAddon; import ch.njol.skript.bstats.bukkit.Metrics; import ch.njol.skript.util.Version; +import com.sovdee.skriptparticles.skript.SkriptParticleModule; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.Nullable; -import java.io.IOException; import java.util.logging.Logger; +/** + * Entry point for the skript-particle plugin. Extends {@link JavaPlugin} and acts as the + * central hub for registering the Skript addon, loading all element classes, and providing + * static logging helpers. + *
+ * On enable, the plugin verifies that Skript 2.7.0 or later is present, registers itself as a + * {@link SkriptAddon}, loads all Skript element classes under {@code com.sovdee.skriptparticles}, + * and initialises bStats metrics. The static {@link #getInstance()} and {@link #getAddonInstance()} + * accessors return {@code null} after the plugin has been disabled. + */ public class SkriptParticle extends JavaPlugin { private static SkriptParticle instance; @@ -29,34 +39,70 @@ public class SkriptParticle extends JavaPlugin { // gradients // text rendering + /** + * Returns the singleton instance of this plugin, or {@code null} if the plugin has not yet + * been enabled or has already been disabled. + * + * @return the current {@link SkriptParticle} instance, or {@code null} + */ @Nullable public static SkriptParticle getInstance() { return instance; } + /** + * Returns the {@link SkriptAddon} registered for this plugin, or {@code null} if the plugin + * has not yet been enabled or has already been disabled. + * + * @return the registered {@link SkriptAddon}, or {@code null} + */ @Nullable public static SkriptAddon getAddonInstance() { return addon; } + /** + * Logs an informational message to the plugin logger. Does nothing if the logger has not yet + * been initialised (i.e. before {@link #onEnable()} has run). + * + * @param message the message to log + */ public static void info(String message) { if (logger == null) return; logger.info(message); } + /** + * Logs a warning message to the plugin logger. Does nothing if the logger has not yet been + * initialised. + * + * @param message the message to log + */ public static void warning(String message) { if (logger == null) return; logger.warning(message); } + /** + * Logs a severe (error-level) message to the plugin logger. Does nothing if the logger has not + * yet been initialised. + * + * @param message the message to log + */ public static void severe(String message) { if (logger == null) return; logger.severe(message); } + /** + * Logs a debug message at INFO level, but only when Skript's debug mode is active. + * Does nothing if the logger has not yet been initialised. + * + * @param message the message to log + */ public static void debug(String message) { if (logger == null) return; @@ -65,6 +111,11 @@ public static void debug(String message) { } } + /** + * Initialises the plugin. Verifies that Skript 2.7.0+ is present, registers the + * {@link SkriptAddon}, loads all element classes under {@code com.sovdee.skriptparticles}, + * and starts bStats metrics collection. Disables the plugin if any prerequisite check fails. + */ @Override public void onEnable() { final PluginManager manager = this.getServer().getPluginManager(); @@ -82,17 +133,15 @@ public void onEnable() { instance = this; addon = Skript.registerAddon(this); addon.setLanguageFileDirectory("lang"); - try { - addon.loadClasses("com.sovdee.skriptparticles"); - } catch (IOException error) { - error.printStackTrace(); - manager.disablePlugin(this); - return; - } + addon.loadModules(new SkriptParticleModule()); new Metrics(this, 18457); SkriptParticle.info("Successfully enabled skript-particle."); } + /** + * Cleans up plugin state on shutdown. Clears the singleton {@link #instance} and + * {@link #addon} references so they become {@code null} for the remainder of the JVM session. + */ @Override public void onDisable() { instance = null; diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffSetOrdering.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffSetOrdering.java deleted file mode 100644 index c22004b..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffSetOrdering.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.sovdee.skriptparticles.elements.effects; - -import ch.njol.skript.Skript; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.util.Kleenean; -import com.sovdee.shapes.shapes.Shape; -import org.bukkit.event.Event; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joml.Vector3d; - -import java.util.Comparator; - -@Name("Shape Animation Ordering") -@Description({ - "Controls the order in which the draw animation effect will draw points. Currently WIP, only supports 2 special orderings.", - "lowest-to-highest, which draws from -x -y -z to x y z, and the reverse." -}) -@Examples( - "set the animation order of {_circle} to lowest-to-highest" -) -@Since("1.3.0") -public class EffSetOrdering extends Effect { - - static { - Skript.registerEffect(EffSetOrdering.class, - "set the animation order of %shapes% to (default|1:lowest-to-highest|2:highest-to-lowest)"); - } - - private Expression shapes; - private int order; - - @Override - public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - shapes = (Expression) expressions[0]; - order = parseResult.mark; - return true; - } - - @Override - protected void execute(Event event) { - @Nullable Comparator order = switch (this.order) { - case 1 -> (o1, o2) -> { - double value1 = o1.x + o1.y + o1.z; - double value2 = o2.x + o2.y + o2.z; - return Double.compare(value1, value2); - }; - case 2 -> (o1, o2) -> { - double value1 = o1.x + o1.y + o1.z; - double value2 = o2.x + o2.y + o2.z; - return -1 * Double.compare(value1, value2); - }; - default -> null; - }; - for (Shape shape : shapes.getArray(event)) { - shape.getPointSampler().setOrdering(order); - } - } - - @Override - public String toString(@Nullable Event event, boolean debug) { - return "set the animation order of " + shapes.toString(event, debug) + " to " + switch (order) { - case 0 -> "default"; - case 1 -> "lowest-to-highest"; - case 2 -> "highest-to-lowest"; - default -> "error"; - }; - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprLastCreatedParticle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprLastCreatedParticle.java deleted file mode 100644 index 9df8fb4..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprLastCreatedParticle.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sovdee.skriptparticles.elements.expressions; - -import ch.njol.skript.Skript; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.util.Kleenean; -import com.sovdee.skriptparticles.elements.sections.SecParticle; -import com.sovdee.skriptparticles.particles.Particle; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -@Name("Last Created Particle") -@Description("Returns the last particle created with the custom particle section.") -@Examples("set {_particle} to last created particle") -@Since("1.0.2") -public class ExprLastCreatedParticle extends SimpleExpression { - - static { - Skript.registerExpression(ExprLastCreatedParticle.class, Particle.class, ExpressionType.SIMPLE, "last created [custom] particle"); - } - - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - return true; - } - - @Override - @Nullable - protected Particle[] get(Event event) { - return new Particle[]{SecParticle.lastCreatedParticle}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Particle.class; - } - - @Override - public String toString(@Nullable Event e, boolean debug) { - return "last created particle"; - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRegularPolyhedron.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRegularPolyhedron.java deleted file mode 100644 index a00c78f..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRegularPolyhedron.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; - -import ch.njol.skript.Skript; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; -import ch.njol.skript.doc.Name; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.Literal; -import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.util.Kleenean; -import com.sovdee.shapes.shapes.RegularPolyhedron; -import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -@Name("Particle Regular Polyhedron") -@Description({ - "Creates a regular polyhedron shape with the given radius. The radius must be greater than 0.", - "Valid polyhedra are tetrahedra (4 faces), octahedra (8), dodecahedra (12), and icosahedra (20).", - "", - "Polyhedra currently do not support the particle count expression, only particle density." -}) -@Examples({ - "set {_shape} to a tetrahedron with radius 1", - "set {_shape} to a solid icosahedron with radius 2", - "draw the shape of a tetrahedron with radius 5 at player" -}) -public class ExprRegularPolyhedron extends SimpleExpression { - - static { - Skript.registerExpression(ExprRegularPolyhedron.class, Shape.class, ExpressionType.COMBINED, "[a[n]] [outlined|:hollow|:solid] (:tetra|:octa|:icosa|:dodeca)hedron (with|of) radius %number%"); - } - - private Expression radius; - private int faces; - private SamplingStyle style; - - @Override - public boolean init(Expression[] expressions, int i, Kleenean kleenean, SkriptParser.ParseResult parseResult) { - radius = (Expression) expressions[0]; - faces = parseResult.hasTag("tetra") ? 4 : parseResult.hasTag("octa") ? 8 : parseResult.hasTag("dodeca") ? 12 : 20; - style = parseResult.hasTag("hollow") ? SamplingStyle.SURFACE : parseResult.hasTag("solid") ? SamplingStyle.FILL : SamplingStyle.OUTLINE; - - if (radius instanceof Literal literal && literal.getSingle().doubleValue() <= 0) { - Skript.error("The radius of the polyhedron must be greater than 0. (radius: " + - ((Literal) radius).getSingle().doubleValue() + ")"); - return false; - } - - return true; - } - - @Override - protected @Nullable Shape[] get(Event event) { - if (radius.getSingle(event) == null) - return new Shape[0]; - RegularPolyhedron shape = new RegularPolyhedron(radius.getSingle(event).doubleValue(), faces); - shape.getPointSampler().setStyle(style); - shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; - } - - @Override - public String toString(@Nullable Event event, boolean b) { - return "regular polyhedron with " + faces + " faces with radius " + radius.toString(event, b); - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/package-info.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/package-info.java deleted file mode 100644 index 4786bc5..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -@DefaultQualifier(NonNull.class) -package com.sovdee.skriptparticles.elements; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/SecParticle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/SecParticle.java deleted file mode 100644 index eea1480..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/SecParticle.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.sovdee.skriptparticles.elements.sections; - -import ch.njol.skript.Skript; -import ch.njol.skript.config.SectionNode; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.Section; -import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.lang.TriggerItem; -import ch.njol.skript.lang.util.SimpleLiteral; -import ch.njol.skript.util.LiteralUtils; -import ch.njol.util.Kleenean; -import com.sovdee.skriptparticles.particles.Particle; -import com.sovdee.skriptparticles.particles.ParticleMotion; -import org.bukkit.event.Event; -import org.bukkit.util.Vector; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; -import org.skriptlang.skript.lang.entry.EntryContainer; -import org.skriptlang.skript.lang.entry.EntryValidator; -import org.skriptlang.skript.lang.entry.util.ExpressionEntryData; - -import java.util.List; - -@Name("Custom Particle Section") -@Description({ - "This section can be used in conjunction with the `last created particle` expression to create custom particles.", - "The particle can be any custom particle from skript-particle or from skbee.", - "Fields include:", - "\tcount: integer - the number of particles to create (required)", - "\toffset: vector - the offset value of the particle. See the Minecraft wiki on /particle for more info. (default: 0, 0, 0)", - "\tvelocity: vector - the velocity of the particle. Can be a vector or a motion (inwards/clockwise/etc.). (default: 0, 0, 0)", - "\textra: number - the extra value of the particle. Forces `count` to be 0 and cannot be combined with `offset`. " + - "See the Minecraft wiki on /particle for more info. (default: 0)", - "\tforce: boolean - whether or not to force the particle to be seen at long range. (default: false)" -}) -@Examples({ - "create a new custom electric spark particle with:", - "\tcount: 10", - "\toffset: vector(1, 1, 1)", - "\textra: 0.2", - "\tforce: true", - "set {_particle} to last created particle", - "", - "create a new custom red dust particle with:", - "\tcount: 0", - "\tvelocity: inwards", - "\textra: 0.5", - "\tforce: true", - "set {_particle} to last created particle", -}) -@Since("1.0.2") -public class SecParticle extends Section { - @Nullable - public static Particle lastCreatedParticle; - private static final EntryValidator validator = EntryValidator.builder() - .addEntryData(new ExpressionEntryData<>("count", new SimpleLiteral<>(1, false), false, Number.class)) - .addEntryData(new ExpressionEntryData<>("offset", null, true, Vector.class)) - .addEntryData(new ExpressionEntryData<>("velocity", null, true, Object.class)) - .addEntryData(new ExpressionEntryData<>("extra", null, true, Number.class)) - .addEntryData(new ExpressionEntryData<>("force", null, true, Boolean.class)) - .build(); - - // Particle section - // create a new %particle% [particle]: - //- count: int - //- offset: vector - //- velocity: vector or inwards/outwards (exclusive w/ offset & count) - //- extra: double - //- - if particle is dust, allow "color: color" and "size: double" - //- force: boolean - static { - Skript.registerSection(SecParticle.class, "create [a] [new] custom %particle% [particle] [with]"); - } - - private Expression particle; - @Nullable - private Expression count; - @Nullable - private Expression offset; - @Nullable - private Expression velocity; - @Nullable - private Expression extra; - @Nullable - private Expression force; - - @SuppressWarnings("unchecked") - @Override - public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult, SectionNode sectionNode, List triggerItems) { - @Nullable EntryContainer entryContainer = validator.validate(sectionNode); - if (entryContainer == null) - return false; - - particle = (Expression) expressions[0]; - count = (Expression) entryContainer.getOptional("count", Expression.class, true); - offset = (Expression) entryContainer.getOptional("offset", Expression.class, true); - extra = (Expression) entryContainer.getOptional("extra", Expression.class, true); - force = (Expression) entryContainer.getOptional("force", Expression.class, true); - - velocity = entryContainer.getOptional("velocity", Expression.class, true); - if (velocity != null) { - velocity = LiteralUtils.defendExpression(velocity); - if (!LiteralUtils.canInitSafely(velocity)){ - Skript.error("Invalid expression for velocity! Must be a vector or a particle motion."); - return false; - } - } - - if (offset != null && velocity != null) { - Skript.error("You cannot have both an offset and a velocity for a particle!"); - return false; - } - - return true; - } - - @Override - @Nullable - protected TriggerItem walk(Event event) { - execute(event); - return walk(event, false); - } - - private void execute(Event event) { - @Nullable ParticleEffect particleEffect = this.particle.getSingle(event); - if (particleEffect == null) - return; - @Nullable Number count = this.count.getSingle(event); - if (count == null) - return; - @Nullable Vector offset = this.offset != null ? this.offset.getSingle(event) : null; - @Nullable Number extra = this.extra != null ? this.extra.getSingle(event) : null; - @Nullable Boolean force = this.force != null ? this.force.getSingle(event) : null; - - Particle particle = Particle.of(particleEffect).count(count.intValue()); - - if (velocity != null) { - @Nullable Object v = velocity.getSingle(event); - if (v instanceof ParticleMotion motion) { - particle.motion(motion); - } else if (v instanceof Vector vector) { - particle.count(0).offset(vector.getX(), vector.getY(), vector.getZ()); - } - } - - if (offset != null) - particle.offset(offset.getX(), offset.getY(), offset.getZ()); - - if (extra != null) - particle.extra(extra.doubleValue()); - - if (force != null) - particle.force(force); - - lastCreatedParticle = particle; - } - - @Override - public String toString(@Nullable Event event, boolean debug) { - return "new custom particle from " + particle.toString(event, debug); - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/ParticleTypes.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/ParticleTypes.java deleted file mode 100644 index f4fa9d2..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/ParticleTypes.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.sovdee.skriptparticles.elements.types; - -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.EnumClassInfo; -import ch.njol.skript.classes.Parser; -import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.lang.util.ContextlessEvent; -import ch.njol.skript.registrations.Classes; -import com.sovdee.skriptparticles.particles.Particle; -import com.sovdee.skriptparticles.particles.ParticleMotion; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; -import org.skriptlang.skript.lang.converter.Converters; - -public class ParticleTypes { - static { - - // Particle Builder class - Classes.registerClass(new ClassInfo<>(Particle.class, "customparticle") - .user("customparticles?") - .name("Custom Particle") - .description("Represents a particle with extra shape-related data, like motion.") - .parser(new Parser<>() { - - @Nullable - @Override - public Particle parse(String s, ParseContext context) { - return null; - } - - @Override - public boolean canParse(ParseContext context) { - return false; - } - - @Override - public @NonNull String toString(Particle particle, int flags) { - return particle.toString(ContextlessEvent.get(), false); - } - - @Override - public @NonNull String toVariableNameString(Particle particle) { - return "particle:" + toString(particle, 0); - } - }) - ); - - // Particle motion class - Classes.registerClass(new EnumClassInfo<>(ParticleMotion.class, "particlemotion", "particle motions") - .user("particle ?motions?") - .name("Particle Motion") - .description("Represents the motion of a particle relative to a shape.") - ); - - Converters.registerConverter(ParticleEffect.class, Particle.class, Particle::of); - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/RotationTypes.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/RotationTypes.java deleted file mode 100644 index 2c22bcc..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/RotationTypes.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.sovdee.skriptparticles.elements.types; - -import ch.njol.skript.Skript; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.Parser; -import ch.njol.skript.expressions.base.EventValueExpression; -import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.lang.function.Functions; -import ch.njol.skript.lang.function.Parameter; -import ch.njol.skript.lang.function.SimpleJavaFunction; -import ch.njol.skript.lang.util.SimpleLiteral; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.registrations.DefaultClasses; -import com.sovdee.skriptparticles.util.Quaternion; -import org.jetbrains.annotations.Nullable; -import org.joml.AxisAngle4f; -import org.joml.Quaternionf; -import org.skriptlang.skript.lang.converter.Converters; - -public class RotationTypes { - static { - if (Classes.getExactClassInfo(Quaternionf.class) == null) { - Classes.registerClass(new ClassInfo<>(Quaternionf.class, "quaternion") - .user("quaternionf?s?") - .name("Quaternion") - .description("Quaternions can be used for shape rotations. They're composed of four values, w, x, y, and z. " + - "See the Quaternion and AxisAngle functions for ways to create them.") - .since("1.0.0") - .parser(new Parser() { - public boolean canParse(ParseContext context) { - return false; - } - - @Override - public String toString(Quaternionf quaternion, int flags) { - return "w:" + Skript.toString(quaternion.w()) + ", x:" + Skript.toString(quaternion.x()) + ", y:" + Skript.toString(quaternion.y()) + ", z:" + Skript.toString(quaternion.z()); - } - - @Override - public String toVariableNameString(Quaternionf quaternion) { - return quaternion.w() + "," + quaternion.x() + "," + quaternion.y() + "," + quaternion.z(); - } - }) - .defaultExpression(new EventValueExpression<>(Quaternionf.class)) - .cloner(quaternion -> { - try { - // Implements cloneable, but doesn't return a Quaternionf. - // org.joml improperly override. Returns Object. - return (Quaternionf) quaternion.clone(); - } catch (CloneNotSupportedException e) { - return null; - } - })); - } - - Converters.registerConverter(Quaternionf.class, Quaternion.class, Quaternion::new); - - if (Functions.getGlobalSignature("quaternion") == null) { - Functions.registerFunction(new SimpleJavaFunction<>("quaternion", new Parameter[]{ - new Parameter<>("x", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), - new Parameter<>("y", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), - new Parameter<>("z", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), - new Parameter<>("w", DefaultClasses.NUMBER, true, new SimpleLiteral(1, true)) - }, Classes.getExactClassInfo(Quaternionf.class), true) { - @Override - public @Nullable Quaternionf[] executeSimple(Object[][] params) { - float w = ((Number) params[0][0]).floatValue(); - float x = ((Number) params[1][0]).floatValue(); - float y = ((Number) params[2][0]).floatValue(); - float z = ((Number) params[3][0]).floatValue(); - return new Quaternionf[]{new Quaternionf(x, y, z, w)}; - } - } - .description("Returns a quaternion from the given x, y, z and w parameters.") - .examples("set {_v} to quaternion(0,0,0,1)") - .since("1.0.0")); - } - - if (Functions.getGlobalSignature("axisAngle") == null) { - Functions.registerFunction(new SimpleJavaFunction<>("axisAngle", new Parameter[]{ - new Parameter<>("angle", DefaultClasses.NUMBER, true, null), - new Parameter<>("x", DefaultClasses.NUMBER, true, null), - new Parameter<>("y", DefaultClasses.NUMBER, true, null), - new Parameter<>("z", DefaultClasses.NUMBER, true, null) - }, Classes.getExactClassInfo(Quaternionf.class), true) { - @Override - public @Nullable Quaternionf[] executeSimple(Object[][] params) { - float angle = ((Number) params[0][0]).floatValue(); - float x = ((Number) params[1][0]).floatValue(); - float y = ((Number) params[2][0]).floatValue(); - float z = ((Number) params[3][0]).floatValue(); - AxisAngle4f axisAngle4f = new AxisAngle4f(angle, x, y, z); - return new Quaternionf[]{new Quaternionf(axisAngle4f)}; - } - } - .description("Returns a quaternion from the given axis and angle parameters. The axis is a vector composed of 3 numbers, x, y, and z, and the angle is the rotation around that axis, in radians.") - .examples("set {_v} to axisAngle(3.14, 1, 0, 0)") - .since("1.0.0")); - } - - if (Functions.getGlobalSignature("axisAngleDegrees") == null) { - Functions.registerFunction(new SimpleJavaFunction<>("axisAngleDegrees", new Parameter[]{ - new Parameter<>("angle", DefaultClasses.NUMBER, true, null), - new Parameter<>("x", DefaultClasses.NUMBER, true, null), - new Parameter<>("y", DefaultClasses.NUMBER, true, null), - new Parameter<>("z", DefaultClasses.NUMBER, true, null) - }, Classes.getExactClassInfo(Quaternionf.class), true) { - @Override - public @Nullable Quaternionf[] executeSimple(Object[][] params) { - float angle = ((Number) params[0][0]).floatValue() * (float) Math.PI / 180; - float x = ((Number) params[1][0]).floatValue(); - float y = ((Number) params[2][0]).floatValue(); - float z = ((Number) params[3][0]).floatValue(); - AxisAngle4f axisAngle4f = new AxisAngle4f(angle, x, y, z); - return new Quaternionf[]{new Quaternionf(axisAngle4f)}; - } - } - .description("Returns a quaternion from the given axis and angle parameters. The axis is a vector composed of 3 numbers, x, y, and z, and the angle is the rotation around that axis, in degrees.") - .examples("set {_v} to axisAngleDegrees(180, 1, 0, 0)") - .since("1.0.0")); - } - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/ShapeTypes.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/ShapeTypes.java deleted file mode 100644 index 57fecf5..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/types/ShapeTypes.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.sovdee.skriptparticles.elements.types; - -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.Parser; -import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.registrations.Classes; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.shapes.shapes.CutoffShape; -import com.sovdee.shapes.shapes.LWHShape; -import com.sovdee.shapes.shapes.PolyShape; -import com.sovdee.shapes.shapes.RadialShape; -import com.sovdee.shapes.shapes.Shape; -import org.jetbrains.annotations.Nullable; - -public class ShapeTypes { - static { - // Shape — register the library shape directly - Classes.registerClass(new ClassInfo<>(Shape.class, "shape") - .user("shapes?") - .name("Shape") - .description("Represents an abstract particle shape. E.g. circle, line, etc.") - .parser(new Parser<>() { - - @Override - public Shape parse(String input, ParseContext context) { - return null; - } - - @Override - public boolean canParse(ParseContext context) { - return false; - } - - @Override - public String toString(Shape o, int flags) { - return o.toString(); - } - - @Override - public String toVariableNameString(Shape shape) { - return "shape:" + shape.getPointSampler().getUUID(); - } - }) - .cloner(Shape::clone) - ); - - // RadialShape - Classes.registerClass(new ClassInfo<>(RadialShape.class, "radialshape") - .user("radial ?shapes?") - .name("Radial Shape") - .description("Represents an abstract particle shape that has a radius. E.g. circle, sphere, etc.") - .parser(new Parser<>() { - @Override - public RadialShape parse(String input, ParseContext context) { return null; } - @Override - public boolean canParse(ParseContext context) { return false; } - @Override - public String toString(RadialShape o, int flags) { return o.toString(); } - @Override - public String toVariableNameString(RadialShape shape) { return "shape:" + shape.getPointSampler().getUUID(); } - }) - ); - - // LWHShape - Classes.registerClass(new ClassInfo<>(LWHShape.class, "lwhshape") - .user("lwh ?shapes?") - .name("Length/Width/Height Shape") - .description("Represents an abstract particle shape that has a length, width, and/or height. E.g. cube, cylinder, ellipse, etc.") - .parser(new Parser<>() { - @Override - public LWHShape parse(String input, ParseContext context) { return null; } - @Override - public boolean canParse(ParseContext context) { return false; } - @Override - public String toString(LWHShape o, int flags) { return o.toString(); } - @Override - public String toVariableNameString(LWHShape shape) { return "shape:" + shape.getPointSampler().getUUID(); } - }) - ); - - // CutoffShape - Classes.registerClass(new ClassInfo<>(CutoffShape.class, "cutoffshape") - .user("cutoff ?shapes?") - .name("Cutoff Shape") - .description("Represents an abstract particle shape that has a cutoff angle. E.g. arc, spherical cap, etc.") - .parser(new Parser<>() { - @Override - public CutoffShape parse(String input, ParseContext context) { return null; } - @Override - public boolean canParse(ParseContext context) { return false; } - @Override - public String toString(CutoffShape o, int flags) { return o.toString(); } - @Override - public String toVariableNameString(CutoffShape shape) { return "shape:" + shape.getPointSampler().getUUID(); } - }) - ); - - // PolyShape - Classes.registerClass(new ClassInfo<>(PolyShape.class, "polyshape") - .user("poly ?shapes?") - .name("Polygonal/Polyhedral Shape") - .description( - "Represents an abstract particle shape that is a polygon or polyhedron, with a side length and side count.\n" + - "Irregular shapes are included in this category, but do not support changing either side count or side length." - ) - .parser(new Parser<>() { - @Override - public PolyShape parse(String input, ParseContext context) { return null; } - @Override - public boolean canParse(ParseContext context) { return false; } - @Override - public String toString(PolyShape o, int flags) { return o.toString(); } - @Override - public String toVariableNameString(PolyShape shape) { return "shape:" + shape.getPointSampler().getUUID(); } - }) - ); - - // Style — use standalone SamplingStyle enum - Classes.registerClass(new ClassInfo<>(SamplingStyle.class, "shapestyle") - .user("shape ?styles?") - .name("Shape Style") - .description("Represents the way the shape is drawn. Outlined is a wireframe representation, Surface is filling in all the surfaces of the shape, and Filled is filling in the entire shape.") - .parser(new Parser<>() { - @Override - public @Nullable SamplingStyle parse(String s, ParseContext context) { - s = s.toUpperCase(); - if (s.matches("OUTLINE(D)?") || s.matches("WIREFRAME")) { - return SamplingStyle.OUTLINE; - } else if (s.matches("SURFACE") || s.matches("HOLLOW")) { - return SamplingStyle.SURFACE; - } else if (s.matches("FILL(ED)?") || s.matches("SOLID")) { - return SamplingStyle.FILL; - } - return null; - } - - @Override - public boolean canParse(ParseContext context) { - return true; - } - - @Override - public String toString(SamplingStyle style, int i) { - return style.toString(); - } - - @Override - public String toVariableNameString(SamplingStyle style) { - return "shapestyle:" + style; - } - })); - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/Particle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/Particle.java deleted file mode 100644 index 3d16431..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/Particle.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.sovdee.skriptparticles.particles; - -import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.shapes.DrawData; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.util.Vector; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; - -import java.util.Collection; -import java.util.List; - -public class Particle extends ParticleEffect { - - private @Nullable ParticleMotion motion; - private @Nullable ParticleGradient gradient; - private @Nullable Shape parent; - private boolean override = false; - - public static Particle of(ParticleEffect effect) { - org.bukkit.Particle effectParticle = effect.particle(); - Particle particle = new Particle(effectParticle); - particle.count(effect.count()); - particle.data(effect.data()); - @Nullable Location loc; - if ((loc = effect.location()) != null) { - particle.location(loc); - } - - particle.offset(effect.offsetX(), effect.offsetY(), effect.offsetZ()); - particle.extra(effect.extra()); - particle.force(effect.force()); - particle.receivers(effect.receivers()); - particle.source(effect.source()); - return particle; - } - - public Particle(org.bukkit.Particle particle) { - super(particle); - } - - public Particle(org.bukkit.Particle particle, ParticleMotion motion) { - super(particle); - this.motion = motion; - } - - public void spawn(Vector delta) { - if (parent == null) return; - DrawData dd = DrawData.of(parent); - if (dd.getLastLocation() == null) return; - if (motion != null) { - Vector yAxis = dd.getLastOrientation().transform(new Vector(0, 1, 0)); - Vector motionVector = motion.getMotionVector(yAxis, delta); - this.offset(motionVector.getX(), motionVector.getY(), motionVector.getZ()); - this.count(0); - } - if (gradient != null) { - color(gradient.calculateColour(delta)); - } - location(dd.getLastLocation().getLocation().add(delta)); - super.spawn(); - } - - @Nullable - public ParticleMotion motion() { - return motion; - } - - public Particle motion(@Nullable ParticleMotion motion) { - this.motion = motion; - return this; - } - - @Nullable - public Shape parent() { - return parent; - } - - public Particle parent(@Nullable Shape parent) { - this.parent = parent; - return this; - } - - @Nullable - public ParticleGradient gradient() { - return gradient; - } - - public Particle gradient(@Nullable ParticleGradient gradient) { - this.gradient = gradient; - return this; - } - - public boolean override() { - return override; - } - - public Particle override(boolean override) { - this.override = override; - return this; - } - - @Contract("-> new") - public Particle clone() { - Particle particle = (Particle) new Particle(this.particle()) - .count(this.count()) - .extra(this.extra()) - .offset(this.offsetX(), this.offsetY(), this.offsetZ()) - .data(this.data()) - .force(this.force()) - .receivers(this.receivers()) - .source(this.source()); - @Nullable Location location = this.location(); - if (location != null) - particle.location(location); - - return particle.motion(this.motion()) - .parent(this.parent()) - .gradient(this.gradient()) - .override(this.override()); - } - - @Override - public String toString() { - return "Particle{" + - "particle=" + this.particle() + - (motion != null ? ", motion=" + motion : "") + - (gradient != null ? ", gradient=" + gradient : "") + - (parent != null ? ", parent=" + parent : "") + - ", override=" + override + - '}'; - } - - // - - @Override - public Particle particle(org.bukkit.Particle particle) { - return (Particle) super.particle(particle); - } - - @Override - public Particle allPlayers() { - return (Particle) super.allPlayers(); - } - - @Override - public Particle receivers(@Nullable List receivers) { - return (Particle) super.receivers(receivers); - } - - @Override - public Particle receivers(@Nullable Collection receivers) { - return (Particle) super.receivers(receivers); - } - - @Override - public Particle receivers(Player @Nullable ... receivers) { - return (Particle) super.receivers(receivers); - } - - @Override - public Particle receivers(int radius) { - return (Particle) super.receivers(radius); - } - - @Override - public Particle receivers(int radius, boolean byDistance) { - return (Particle) super.receivers(radius, byDistance); - } - - @Override - public Particle receivers(int xzRadius, int yRadius) { - return (Particle) super.receivers(xzRadius, yRadius); - } - - @Override - public Particle receivers(int xzRadius, int yRadius, boolean byDistance) { - return (Particle) super.receivers(xzRadius, yRadius, byDistance); - } - - @Override - public Particle receivers(int xRadius, int yRadius, int zRadius) { - return (Particle) super.receivers(xRadius, yRadius, zRadius); - } - - @Override - public Particle source(@Nullable Player source) { - return (Particle) super.source(source); - } - - @Override - public Particle location(Location location) { - return (Particle) super.location(location); - } - - @Override - public Particle location(World world, double x, double y, double z) { - return (Particle) super.location(world, x, y, z); - } - - @Override - public Particle count(int count) { - return (Particle) super.count(count); - } - - @Override - public Particle offset(double offsetX, double offsetY, double offsetZ) { - return (Particle) super.offset(offsetX, offsetY, offsetZ); - } - - @Override - public ParticleEffect extra(double extra) { - return (ParticleEffect) super.extra(extra); - } - - @Override - public Particle force(boolean force) { - return (Particle) super.force(force); - } - // - -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/ParticleGradient.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/ParticleGradient.java deleted file mode 100644 index e6d8c3f..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/ParticleGradient.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.sovdee.skriptparticles.particles; - -import ch.njol.skript.util.ColorRGB; -import com.sovdee.skriptparticles.util.Quaternion; -import org.bukkit.Color; -import org.bukkit.util.Vector; - -import java.util.ArrayList; -import java.util.List; - -public class ParticleGradient { - - private final Quaternion orientation = new Quaternion(1, 0, 0, 0); - private final List points = new ArrayList<>(); - private boolean local = false; - - public Color calculateColour(Vector delta) { - if (local) { - orientation.transform(delta); - } - - double weightTotal = 0; - double[] rgb = new double[]{0, 0, 0}; - double weight; - Color colour; - for (Point point : points) { - weight = (1 / (point.getPosition().clone().subtract(delta).length())); - weightTotal += weight; - colour = point.getColor(); - rgb[0] += weight * colour.getRed(); - rgb[1] += weight * colour.getGreen(); - rgb[2] += weight * colour.getBlue(); - } - return Color.fromRGB((int) (rgb[0] / weightTotal), (int) (rgb[1] / weightTotal), (int) (rgb[2] / weightTotal)); - } - - public Quaternion getOrientation() { - return orientation; - } - - public void setOrientation(Quaternion orientation) { - this.orientation.set(orientation.clone()); - } - - public List getPoints() { - return points; - } - - public void setPoints(List points) { - this.points.clear(); - this.points.addAll(points); - } - - public void addPoint(Vector position, Color color) { - points.add(new Point(position, color)); - } - - public boolean isLocal() { - return local; - } - - public void setLocal(boolean local) { - this.local = local; - } - - @Override - public String toString() { - return "ParticleGradient{" + - "orientation=" + orientation + - ", points=" + points + - ", local=" + local + - '}'; - } - - public static class Point { - - private Vector position; - private Color color; - - public Point(Vector position, Color color) { - this.position = position; - this.color = color; - } - - public Point(Vector position, ColorRGB color) { - this.position = position; - this.color = color.asBukkitColor(); - } - - public Vector getPosition() { - return position; - } - - public void setPosition(Vector position) { - this.position = position; - } - - public Color getColor() { - return color; - } - - public void setColor(Color color) { - this.color = color; - } - - @Override - public String toString() { - return "Point{" + - "position=" + position + - ", color=" + color + - '}'; - } - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/ParticleMotion.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/ParticleMotion.java deleted file mode 100644 index a60efb7..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/ParticleMotion.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sovdee.skriptparticles.particles; - - -import org.bukkit.util.Vector; - -public enum ParticleMotion { - CLOCKWISE, - COUNTERCLOCKWISE, - INWARDS, - OUTWARDS, - NONE; - - private static final Vector DEFAULT_MOTION = new Vector(0, 0, 0); - - public Vector getMotionVector(Vector axis, Vector point) { - return switch (this) { - case NONE -> DEFAULT_MOTION.clone(); - case CLOCKWISE -> getAntiClockwiseMotion(axis, point).multiply(-1); - case COUNTERCLOCKWISE -> getAntiClockwiseMotion(axis, point); - case INWARDS -> getOutwardsMotion(point).multiply(-1); - case OUTWARDS -> getOutwardsMotion(point); - }; - } - - private Vector getAntiClockwiseMotion(Vector axis, Vector point) { - return axis.getCrossProduct(point).normalize(); - } - - private Vector getOutwardsMotion(Vector point) { - return point.clone().normalize(); - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/package-info.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/package-info.java deleted file mode 100644 index cc50bf3..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/particles/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -@DefaultQualifier(NonNull.class) -package com.sovdee.skriptparticles.particles; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/DrawData.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/DrawData.java new file mode 100644 index 0000000..49d9756 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/DrawData.java @@ -0,0 +1,248 @@ +package com.sovdee.skriptparticles.rendering; + +import com.sovdee.shapes.sampling.DrawContext; +import com.sovdee.shapes.shapes.Shape; +import com.sovdee.skriptparticles.util.DynamicLocation; +import com.sovdee.skriptparticles.util.Quaternion; +import org.jetbrains.annotations.Nullable; + +/** + * Plugin-side rendering metadata attached to library shapes via {@link DrawContext}. + * Holds particle, location, animation, and debug axis state. + * Render modifiers live on the shape's {@link com.sovdee.shapes.sampling.PointSampler}. + */ +public class DrawData implements DrawContext { + + private Particle particle; + private @Nullable DynamicLocation location; + private @Nullable DynamicLocation lastLocation; + private final Quaternion lastOrientation; + private long animationDuration = 0; + private boolean drawLocalAxes = false; + private boolean drawGlobalAxes = false; + /** + * Set to true after a large-point-count warning has been emitted for this shape. + */ + private boolean largeSizeWarned = false; + + /** + * Creates a new {@code DrawData} with default settings: a FLAME particle with zero extra + * speed, no location, and an identity last-orientation quaternion. + */ + public DrawData() { + this.particle = new Particle(org.bukkit.Particle.FLAME).extra(0); + this.lastOrientation = Quaternion.IDENTITY.clone(); + } + + /** + * Gets the DrawData attached to a shape's PointSampler, creating and attaching one if missing. + */ + public static DrawData of(Shape shape) { + DrawContext ctx = shape.getPointSampler().getDrawContext(); + if (ctx instanceof DrawData dd) return dd; + DrawData dd = new DrawData(); + shape.getPointSampler().setDrawContext(dd); + return dd; + } + + // ---- Particle ---- + + /** + * Returns a clone of the particle stored in this draw data. Callers may modify the returned + * instance without affecting the stored particle. + * + * @return a cloned copy of the particle + */ + public Particle getParticle() { + return particle.clone(); + } + + /** + * Returns the raw (uncloned) particle stored in this draw data. Modifications to the returned + * instance will directly affect the stored particle. + * + * @return the stored {@link Particle} instance + */ + public Particle getParticleRaw() { + return particle; + } + + /** + * Replaces the stored particle with the given instance. + * + * @param particle the new particle to store + */ + public void setParticle(Particle particle) { + this.particle = particle; + } + + // ---- Location ---- + + /** + * Returns a clone of the target draw location, or {@code null} if no location has been set. + * + * @return a cloned {@link DynamicLocation}, or {@code null} + */ + @Nullable + public DynamicLocation getLocation() { + if (location == null) return null; + return location.clone(); + } + + /** + * Sets the target draw location for this shape. + * + * @param location the {@link DynamicLocation} to draw at + */ + public void setLocation(DynamicLocation location) { + this.location = location; + } + + /** + * Returns the location that was active during the most recent draw call, or {@code null} if + * the shape has not yet been drawn. + * + * @return the last draw location, or {@code null} + */ + @Nullable + public DynamicLocation getLastLocation() { + return lastLocation; + } + + /** + * Records the location used during the most recent draw call. Called internally by + * {@link DrawManager}. + * + * @param lastLocation the location to store as the last draw location, or {@code null} + */ + public void setLastLocation(@Nullable DynamicLocation lastLocation) { + this.lastLocation = lastLocation; + } + + // ---- Orientation ---- + + /** + * Returns the combined orientation quaternion that was used during the most recent draw call. + * The returned instance is the live object; mutating it affects the stored value. + * + * @return the last draw orientation as a {@link Quaternion} + */ + public Quaternion getLastOrientation() { + return lastOrientation; + } + + /** + * Updates the stored last-orientation quaternion to match the given value. + * + * @param orientation the orientation to store + */ + public void setLastOrientation(Quaternion orientation) { + this.lastOrientation.set(orientation); + } + + // ---- Animation ---- + + /** + * Returns the animation duration in milliseconds. A value of 0 means the shape is drawn + * instantaneously rather than animated. + * + * @return the animation duration in milliseconds + */ + public long getAnimationDuration() { + return animationDuration; + } + + /** + * Sets the animation duration in milliseconds. Set to 0 to disable animation. + * + * @param animationDuration the animation duration in milliseconds + */ + public void setAnimationDuration(long animationDuration) { + this.animationDuration = animationDuration; + } + + // ---- Axes ---- + + /** + * Returns {@code true} if the shape's local orientation axes should be drawn as debug + * particles after rendering the shape. + * + * @return {@code true} if local axes are drawn + */ + public boolean showLocalAxes() { + return drawLocalAxes; + } + + /** + * Sets whether the shape's local orientation axes should be drawn as debug particles. + * + * @param show {@code true} to draw local axes + */ + public void showLocalAxes(boolean show) { + this.drawLocalAxes = show; + } + + /** + * Returns {@code true} if the world-space global axes should be drawn as debug particles + * at the shape's origin after rendering. + * + * @return {@code true} if global axes are drawn + */ + public boolean showGlobalAxes() { + return drawGlobalAxes; + } + + /** + * Sets whether the global (world-space) axes should be drawn as debug particles. + * + * @param show {@code true} to draw global axes + */ + public void showGlobalAxes(boolean show) { + this.drawGlobalAxes = show; + } + + // ---- Warning flag ---- + + /** + * Returns {@code true} if a large-particle-count warning has already been emitted for this + * shape during the current session. Used to prevent repeated warnings. + * + * @return {@code true} if the large-size warning has been emitted + */ + public boolean isLargeSizeWarned() { + return largeSizeWarned; + } + + /** + * Sets the large-size warning flag to suppress duplicate warnings. + * + * @param warned {@code true} to mark the warning as already emitted + */ + public void setLargeSizeWarned(boolean warned) { + this.largeSizeWarned = warned; + } + + // ---- DrawContext ---- + + /** + * Creates a deep copy of this {@code DrawData}, duplicating the particle, location, + * last location, last orientation, animation duration, and axis-draw flags. The + * {@link #isLargeSizeWarned()} flag is not copied and resets to {@code false} in the clone. + * + * @return a new {@code DrawData} with the same settings as this instance + */ + @Override + public DrawData copy() { + DrawData copy = new DrawData(); + copy.particle = this.particle.clone(); + if (this.location != null) + copy.location = this.location.clone(); + if (this.lastLocation != null) + copy.lastLocation = this.lastLocation.clone(); + copy.lastOrientation.set(this.lastOrientation); + copy.animationDuration = this.animationDuration; + copy.drawLocalAxes = this.drawLocalAxes; + copy.drawGlobalAxes = this.drawGlobalAxes; + return copy; + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/DrawManager.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/DrawManager.java new file mode 100644 index 0000000..fd55199 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/DrawManager.java @@ -0,0 +1,257 @@ +package com.sovdee.skriptparticles.rendering; + +import ch.njol.skript.Skript; +import com.sovdee.shapes.shapes.Shape; +import com.sovdee.skriptparticles.util.DynamicLocation; +import com.sovdee.skriptparticles.util.MathUtil; +import com.sovdee.skriptparticles.util.ParticleUtil; +import com.sovdee.skriptparticles.util.Quaternion; +import com.sovdee.skriptparticles.util.VectorConversion; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.Vector; +import org.joml.Quaterniond; +import org.joml.Vector3d; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + + +/** + * Static draw methods for rendering library shapes with plugin DrawData. + */ +public class DrawManager { + + /** + * Maximum distance (squared) at which a player can see non-forced particles. + * Matches the vanilla client's particle render distance of 32 blocks. + */ + private static final double MAX_PARTICLE_DISTANCE_SQ = 32 * 32; + + /** + * Maximum distance (squared) at which a player can see forced particles (256 blocks). + */ + private static final double MAX_FORCED_PARTICLE_DISTANCE_SQ = 256 * 256; + + /** + * Draws {@code shape} at its own stored {@link DrawData} location using the shape's stored + * particle and the identity orientation. Equivalent to calling + * {@link #draw(Shape, DynamicLocation, Quaternion, Particle, Collection)} with the shape's + * own location and particle. + * + * @param shape the shape to draw + * @param recipients the players who should receive the particles + */ + public static void draw(Shape shape, Collection recipients) { + DrawData dd = DrawData.of(shape); + DynamicLocation location = dd.getLocation(); + if (location == null) return; + draw(shape, location, Quaternion.IDENTITY, dd.getParticleRaw(), recipients); + } + + /** + * Draws {@code shape} at the given {@code location} using the shape's stored particle and + * the identity orientation. + * + * @param shape the shape to draw + * @param location the world-space location to draw at + * @param recipients the players who should receive the particles + */ + public static void draw(Shape shape, DynamicLocation location, Collection recipients) { + draw(shape, location, Quaternion.IDENTITY, DrawData.of(shape).getParticleRaw(), recipients); + } + + /** + * Applies {@code consumer} to {@code shape} (e.g. to set its orientation) and then draws it + * at the given {@code location} using the shape's stored particle. + *
+ * The consumer runs synchronously before any rendering begins. The shape's orientation after + * the consumer returns is used as the base orientation for the draw call. + * + * @param shape the shape to draw + * @param location the world-space location to draw at + * @param consumer a callback invoked on the shape immediately before rendering + * @param recipients the players who should receive the particles + */ + public static void drawWithConsumer(Shape shape, DynamicLocation location, Consumer consumer, Collection recipients) { + consumer.accept(shape); + DrawData dd = DrawData.of(shape); + Quaterniond shapeOrientation = shape.getOrientation(); + Quaternion shapeOrientationQ = new Quaternion((float) shapeOrientation.x, (float) shapeOrientation.y, (float) shapeOrientation.z, (float) shapeOrientation.w); + draw(shape, location, shapeOrientationQ, dd.getParticleRaw(), recipients); + } + + /** + * Draws {@code shape} at {@code location} with an explicit base orientation and particle. + * This is the primary draw method; all other overloads delegate to it. + *
+ * If {@code location} is null the shape's stored location is used instead. Recipients are + * pre-filtered by distance from the shape centre before any per-point work is done. If + * {@link DrawData#getAnimationDuration()} is greater than zero the points are spread across + * multiple async ticks; otherwise all points are sent in a single pass. + * + * @param shape the shape to draw + * @param location the world-space location to draw at; falls back to the shape's own + * stored location if {@link DynamicLocation#isNull()} returns true + * @param baseOrientation additional rotation applied on top of the shape's own orientation + * @param particle the particle to use for rendering; ignored when + * {@link Particle#override()} is false and the shape has its own particle + * @param recipients the candidate players to send particles to (filtered by distance) + */ + public static void draw(Shape shape, DynamicLocation location, Quaternion baseOrientation, Particle particle, Collection recipients) { + DrawData dd = DrawData.of(shape); + + if (location.isNull()) { + DynamicLocation shapeLocation = dd.getLocation(); + if (shapeLocation == null) return; + location = shapeLocation.clone(); + } + + dd.setLastLocation(location.clone()); + Quaterniond shapeOrientation = shape.getOrientation(); + Quaternion shapeOrientationQ = new Quaternion((float) shapeOrientation.x, (float) shapeOrientation.y, (float) shapeOrientation.z, (float) shapeOrientation.w); + dd.getLastOrientation().set(baseOrientation.clone().mul(shapeOrientationQ)); + + if (!particle.override()) { + dd.getParticleRaw().parent(shape); + particle = dd.getParticleRaw(); + } + + // Pre-filter recipients once instead of per-point. + Location center = location.getLocation(); + List filteredRecipients = filterRecipients(recipients, center, particle.force()); + + if (filteredRecipients.isEmpty()) return; + + Quaterniond lastOrientationD = new Quaterniond( + dd.getLastOrientation().x, dd.getLastOrientation().y, + dd.getLastOrientation().z, dd.getLastOrientation().w); + + long animationDuration = dd.getAnimationDuration(); + boolean useNMS = NMSParticleRenderer.isAvailable(); + + if (animationDuration > 0) { + // Animated: get points list for batching, then use NMS/Bukkit per batch + List jomlPoints = shape.getPointSampler().getPoints(shape, lastOrientationD); + drawAnimated(shape, jomlPoints, lastOrientationD, animationDuration, particle, dd, + filteredRecipients, useNMS, center); + } else { + drawImmediate(shape, lastOrientationD, particle, dd, filteredRecipients, useNMS, center); + } + + if (dd.showLocalAxes()) { + ParticleUtil.drawAxes(location.getLocation().add(VectorConversion.toBukkit(shape.getOffset())), dd.getLastOrientation(), filteredRecipients); + } + if (dd.showGlobalAxes()) { + ParticleUtil.drawAxes(location.getLocation().add(VectorConversion.toBukkit(shape.getOffset())), Quaternion.IDENTITY, filteredRecipients); + } + } + + /** + * Draws all points immediately (non-animated). + * NMS path: uses the library render loop via shape.render(). + * Fallback path: converts JOML points to Bukkit Vectors one at a time. + */ + private static void drawImmediate(Shape shape, Quaterniond orientation, Particle particle, + DrawData dd, List filteredRecipients, boolean useNMS, + Location baseLoc) { + try { + if (useNMS) { + NMSParticleRenderer renderer = new NMSParticleRenderer(particle, dd, filteredRecipients, baseLoc); + ParticleRenderContext ctx = renderer.getContext(); + ctx.orientation = orientation; + ctx.scale = shape.getScale(); + shape.render(orientation, renderer); + } else { + List jomlPoints = shape.getPointSampler().getPoints(shape, orientation); + for (Vector3d point : jomlPoints) { + particle.prepareForPoint(dd, new Vector(point.x, point.y, point.z)); + particle.spawnToPlayers(filteredRecipients); + } + } + } catch (IllegalArgumentException e) { + Skript.error("Failed to spawn particle! Error: " + e.getMessage()); + } + } + + /** + * Draws points over time using batched async ticks. + * NMS: uses the library render loop with sub-range batching. + * Fallback: converts JOML points to Bukkit Vectors per batch. + */ + private static void drawAnimated(Shape shape, List jomlPoints, Quaterniond orientation, + long animationDuration, Particle particle, DrawData dd, + List filteredRecipients, boolean useNMS, Location baseLoc) { + int particleCount = jomlPoints.size(); + double millisecondsPerPoint = animationDuration / (double) particleCount; + Iterator> batchIterator = MathUtil.batch(jomlPoints, millisecondsPerPoint).iterator(); + + BukkitRunnable runnable = new BukkitRunnable() { + @Override + public void run() { + if (!batchIterator.hasNext()) { + this.cancel(); + return; + } + List batch = batchIterator.next(); + try { + if (useNMS) { + // Use a simple inline renderer for batch sub-lists + sendBatchNMS(batch, particle, dd, filteredRecipients, baseLoc); + } else { + for (Vector3d point : batch) { + particle.prepareForPoint(dd, new Vector(point.x, point.y, point.z)); + particle.spawnToPlayers(filteredRecipients); + } + } + } catch (IllegalArgumentException e) { + Skript.error("Failed to spawn particle! Error: " + e.getMessage()); + } + } + }; + runnable.runTaskTimerAsynchronously(Skript.getInstance(), 0, 1); + } + + /** + * Sends a pre-computed batch of JOML points via NMS without running the full render pipeline. + * Used by animated drawing where the points are already computed. + */ + private static void sendBatchNMS(List batch, Particle particle, DrawData dd, + List filteredRecipients, Location baseLoc) { + // For batched animation we use NMSParticleRenderer directly on the pre-computed points. + // Render modifiers are skipped for animated batches (they run only on full render passes). + NMSParticleRenderer sender = new NMSParticleRenderer(particle, dd, filteredRecipients, baseLoc); + ParticleRenderContext ctx = new ParticleRenderContext(particle); + sender.begin(batch.size()); + for (int i = 0; i < batch.size(); i++) { + Vector3d p = batch.get(i); + ctx.reset(); + ctx.x = p.x; ctx.y = p.y; ctx.z = p.z; + ctx.index = i; + sender.renderPoint(ctx); + } + sender.end(); + } + + /** + * Filters recipients by distance from the shape center. + * This is done once per shape draw instead of per-point, eliminating redundant checks. + */ + private static List filterRecipients(Collection recipients, Location center, boolean force) { + double maxDistSq = force ? MAX_FORCED_PARTICLE_DISTANCE_SQ : MAX_PARTICLE_DISTANCE_SQ; + List filtered = new ArrayList<>(recipients.size()); + for (Player player : recipients) { + if (!player.isOnline()) continue; + Location playerLoc = player.getLocation(); + if (!center.getWorld().equals(playerLoc.getWorld())) continue; + if (center.distanceSquared(playerLoc) <= maxDistSq) { + filtered.add(player); + } + } + return filtered; + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/NMSParticleRenderer.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/NMSParticleRenderer.java new file mode 100644 index 0000000..c459e1c --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/NMSParticleRenderer.java @@ -0,0 +1,252 @@ +package com.sovdee.skriptparticles.rendering; + +import com.sovdee.shapes.sampling.ShapeRenderer; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBundlePacket; +import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.Location; +import org.bukkit.craftbukkit.CraftParticle; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Sends particles via NMS packets, using direct ByteBuf pipeline injection for optimal performance. + *

+ * Implements {@link ShapeRenderer} so the library's render loop calls {@link #renderPoint} per-point. + * Bundles up to 4096 particles per frame using bundle delimiters. + *

+ * Handles version differences in the packet constructor: + *

    + *
  • 1.21.0 - 1.21.3: {@code (T, boolean overrideLimiter, double x,y,z, float xDist,yDist,zDist,maxSpeed, int count)}
  • + *
  • 1.21.4+: {@code (T, boolean overrideLimiter, boolean alwaysShow, double x,y,z, float xDist,yDist,zDist,maxSpeed, int count)}
  • + *
+ */ +public class NMSParticleRenderer implements ShapeRenderer { + + private static final Logger LOGGER = Logger.getLogger(NMSParticleRenderer.class.getName()); + + /** + * Maximum number of packets per bundle. The protocol supports up to 4096. + */ + private static final int MAX_BUNDLE_SIZE = 4096; + + /** + * Warn when a shape draws more than this many particles in a single call. + */ + private static final int LARGE_PARTICLE_THRESHOLD = 16_000; + + /** + * Functional interface for packet creation, eliminating branching in hot loop. + */ + @FunctionalInterface + private interface PacketFactory { + ClientboundLevelParticlesPacket create( + ParticleOptions options, boolean force, + double x, double y, double z, + float xDist, float yDist, float zDist, float maxSpeed, int count + ) throws Throwable; + } + + /** + * Cached packet factory using the version-appropriate constructor. + * Null if reflection lookup failed (will fall back to Bukkit API). + */ + private static final @Nullable PacketFactory PACKET_FACTORY; + + static { + PacketFactory factory = null; + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + // Try the 2-boolean constructor first (1.21.4+) + MethodHandle ctor = lookup.findConstructor(ClientboundLevelParticlesPacket.class, MethodType.methodType( + void.class, + ParticleOptions.class, boolean.class, boolean.class, + double.class, double.class, double.class, + float.class, float.class, float.class, float.class, + int.class + )); + factory = (options, force, x, y, z, xDist, yDist, zDist, maxSpeed, count) -> + (ClientboundLevelParticlesPacket) ctor.invokeExact( + options, force, force, x, y, z, xDist, yDist, zDist, maxSpeed, count + ); + } catch (NoSuchMethodException | IllegalAccessException e) { + try { + // Fall back to 1-boolean constructor (1.21.0 - 1.21.3) + //noinspection JavaLangInvokeHandleSignature + MethodHandle ctor = lookup.findConstructor(ClientboundLevelParticlesPacket.class, MethodType.methodType( + void.class, + ParticleOptions.class, boolean.class, + double.class, double.class, double.class, + float.class, float.class, float.class, float.class, + int.class + )); + factory = (options, force, x, y, z, xDist, yDist, zDist, maxSpeed, count) -> + (ClientboundLevelParticlesPacket) ctor.invokeExact( + options, force, x, y, z, xDist, yDist, zDist, maxSpeed, count + ); + } catch (NoSuchMethodException | IllegalAccessException ex) { + LOGGER.log(Level.WARNING, "Could not find ClientboundLevelParticlesPacket constructor. " + + "NMS packet bundling will be unavailable.", ex); + } + } + + PACKET_FACTORY = factory; + } + + /** + * Returns true if NMS packet sending is available on this server version. + */ + public static boolean isAvailable() { + return PACKET_FACTORY != null; + } + + // ---- Instance fields (set up in constructor, used across begin/renderPoint/end) ---- + + private final DrawData dd; + private final List serverPlayers; + private final double baseX, baseY, baseZ; + + private final boolean force; + private final float defaultOffsetX, defaultOffsetY, defaultOffsetZ; + private final int defaultCount; + private final float speed; + + /** + * Pre-computed NMS options for the default particle (used when no modifier changed + * particle/data). + */ + private final ParticleOptions defaultOptions; + + private List> chunk; + + // the context to use for rendering + private final ParticleRenderContext context; + + /** + * Creates a renderer instance for one draw call. + * + * @param particle the particle configuration (also stored as defaults in the context) + * @param dd the draw data for the shape (for warning flag) + * @param recipients pre-filtered list of players + * @param baseLoc world-space base location for point offsets + */ + public NMSParticleRenderer(Particle particle, DrawData dd, List recipients, Location baseLoc) { + this.dd = dd; + this.baseX = baseLoc.getX(); + this.baseY = baseLoc.getY(); + this.baseZ = baseLoc.getZ(); + + this.force = particle.force(); + this.defaultOffsetX = (float) particle.offsetX(); + this.defaultOffsetY = (float) particle.offsetY(); + this.defaultOffsetZ = (float) particle.offsetZ(); + this.defaultCount = particle.count(); + this.speed = (float) particle.extra(); + this.defaultOptions = CraftParticle.createParticleParam(particle.particle(), particle.data()); + + this.serverPlayers = new ArrayList<>(recipients.size()); + for (Player player : recipients) { + this.serverPlayers.add(((CraftPlayer) player).getHandle()); + } + + this.context = new ParticleRenderContext(particle); + } + + @Override + public ParticleRenderContext getContext() { + return context; + } + + @Override + public void begin(int totalPoints) { + if (!dd.isLargeSizeWarned() && totalPoints > LARGE_PARTICLE_THRESHOLD) { + LOGGER.warning("Shape is drawing " + totalPoints + " particles in one call. " + + "This may cause lag. Consider reducing particle density or shape size."); + dd.setLargeSizeWarned(true); + } + chunk = new ArrayList<>(MAX_BUNDLE_SIZE); + } + + @Override + public void renderPoint(ParticleRenderContext context) { + if (!context.visible) return; + + float offsetX = defaultOffsetX; + float offsetY = defaultOffsetY; + float offsetZ = defaultOffsetZ; + int count = defaultCount; + + double px = baseX + context.x + context.displacementX; + double py = baseY + context.y + context.displacementY; + double pz = baseZ + context.z + context.displacementZ; + + // Motion override + if (context.hasMotion) { + offsetX = context.motionX; + offsetY = context.motionY; + offsetZ = context.motionZ; + count = 0; + } + + // Use default cached options if no modifier changed the particle; recompute otherwise. + ParticleOptions options = context.isDefaultParticle() + ? defaultOptions + : CraftParticle.createParticleParam(context.particle, context.data); + + ClientboundLevelParticlesPacket pkt = createPacket( + options, force, px, py, pz, offsetX, offsetY, offsetZ, speed, count + ); + chunk.add(pkt); + + if (chunk.size() >= MAX_BUNDLE_SIZE) { + sendChunkToPlayers(chunk, serverPlayers); + chunk = new ArrayList<>(MAX_BUNDLE_SIZE); + } + } + + @Override + public void end() { + if (!chunk.isEmpty()) { + sendChunkToPlayers(chunk, serverPlayers); + } + } + + /** + * Creates a particle packet using the version-appropriate constructor. + */ + private static ClientboundLevelParticlesPacket createPacket( + ParticleOptions options, boolean force, + double x, double y, double z, + float xDist, float yDist, float zDist, float maxSpeed, int count) { + try { + assert PACKET_FACTORY != null; + return PACKET_FACTORY.create(options, force, x, y, z, xDist, yDist, zDist, maxSpeed, count); + } catch (Throwable e) { + LOGGER.log(Level.WARNING, "Failed to create particle packet", e); + return null; + } + } + + /** + * Sends a chunk of packets to all players as a single bundle each. + */ + private static void sendChunkToPlayers(List> chunk, List players) { + ClientboundBundlePacket bundle = new ClientboundBundlePacket(chunk); + for (ServerPlayer player : players) { + player.connection.send(bundle); + } + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/Particle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/Particle.java new file mode 100644 index 0000000..3a3d006 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/Particle.java @@ -0,0 +1,406 @@ +package com.sovdee.skriptparticles.rendering; + +import com.sovdee.shapes.shapes.Shape; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; + +import java.util.Collection; +import java.util.List; + +/** + * Plugin-side particle descriptor extending Skript's {@link ParticleEffect} with a fluent builder API. + * Adds an optional {@link #parent(Shape) parent shape} reference (used to look up {@link DrawData} at + * spawn time) and an {@link #override()} flag that controls whether the draw manager should replace + * this particle with the shape's own stored particle. + *
+ * All mutating methods return {@code this} (or a new {@code Particle}) so calls can be chained. + * Use {@link #of(ParticleEffect)} to promote an existing {@link ParticleEffect} into a {@code Particle}, + * or construct one directly from a Bukkit {@link org.bukkit.Particle} type. + */ +public class Particle extends ParticleEffect { + + private @Nullable Shape parent; + private boolean override = false; + + /** + * Creates a new {@code Particle} by copying all settings from an existing {@link ParticleEffect}. + * Properties copied include particle type, count, data, location, offset, extra speed, force flag, + * receivers, and source player. + * + * @param effect the {@link ParticleEffect} whose settings should be copied + * @return a new {@code Particle} containing all settings from {@code effect} + */ + public static Particle of(ParticleEffect effect) { + org.bukkit.Particle effectParticle = effect.particle(); + Particle particle = new Particle(effectParticle); + particle.count(effect.count()); + particle.data(effect.data()); + @Nullable Location loc; + if ((loc = effect.location()) != null) { + particle.location(loc); + } + + particle.offset(effect.offsetX(), effect.offsetY(), effect.offsetZ()); + particle.extra(effect.extra()); + particle.force(effect.force()); + particle.receivers(effect.receivers()); + particle.source(effect.source()); + return particle; + } + + /** + * Creates a new {@code Particle} wrapping the given Bukkit particle type with default settings + * (count 1, no offset, no extra, no force, no receivers, no source, no location). + * + * @param particle the Bukkit particle type to use + */ + public Particle(org.bukkit.Particle particle) { + super(particle); + } + + /** + * Spawns this particle at the parent shape's last draw location offset by {@code delta}. + * Does nothing if no {@link #parent(Shape) parent shape} has been set or if the parent has + * no recorded last location. + * + * @param delta the offset from the shape's last draw location at which to spawn the particle + */ + public void spawn(org.bukkit.util.Vector delta) { + if (parent == null) return; + DrawData dd = DrawData.of(parent); + if (dd.getLastLocation() == null) return; + location(dd.getLastLocation().getLocation().add(delta)); + super.spawn(); + } + + /** + * Prepares this particle's location for a given point delta without spawning it. + * Call this before using {@link #spawnToPlayers(Collection)} to send to pre-filtered recipients. + */ + public void prepareForPoint(DrawData dd, org.bukkit.util.Vector delta) { + location(dd.getLastLocation().getLocation().add(delta)); + } + + /** + * Spawns the particle to each player individually, bypassing per-player distance/vanish + * checks. Assumes the caller has already pre-filtered the recipient list. + */ + public void spawnToPlayers(Collection players) { + Location loc = location(); + if (loc == null || loc.getWorld() == null) return; + for (Player player : players) { + player.spawnParticle(particle(), loc, count(), offsetX(), offsetY(), offsetZ(), extra(), data()); + } + } + + /** + * Calls the given consumer with the particle type and prepared data for NMS packet construction. + * This allows external code to build packets without going through the Bukkit API. + * + * @param consumer receives the Bukkit particle type and the particle-specific data (may be null) + */ + public void forPacketData(java.util.function.BiConsumer consumer) { + consumer.accept(particle(), data()); + } + + /** + * Returns the shape this particle is associated with, or {@code null} if none has been set. + * The parent is used to retrieve {@link DrawData} (e.g. the last draw location) when + * {@link #spawn(org.bukkit.util.Vector)} is called. + * + * @return the parent {@link Shape}, or {@code null} + */ + @Nullable + public Shape parent() { + return parent; + } + + /** + * Sets the parent shape for this particle and returns {@code this} for chaining. + * + * @param parent the shape to associate with this particle, or {@code null} to clear it + * @return this {@code Particle} instance + */ + public Particle parent(@Nullable Shape parent) { + this.parent = parent; + return this; + } + + /** + * Returns {@code true} if this particle should override the shape's own stored particle when + * passed to the draw manager. When {@code false} (the default), the draw manager replaces this + * particle with the shape's stored particle. + * + * @return {@code true} if this particle overrides the shape's particle + */ + public boolean override() { + return override; + } + + /** + * Sets whether this particle should override the shape's stored particle and returns + * {@code this} for chaining. + * + * @param override {@code true} to use this particle instead of the shape's own particle + * @return this {@code Particle} instance + */ + public Particle override(boolean override) { + this.override = override; + return this; + } + + /** + * Creates a deep copy of this particle, duplicating all settings including particle type, + * count, extra, offset, data, force flag, receivers, source, location, parent shape, and + * override flag. + * + * @return a new {@code Particle} with identical settings + */ + @Contract("-> new") + public Particle clone() { + Particle particle = (Particle) new Particle(this.particle()) + .count(this.count()) + .extra(this.extra()) + .offset(this.offsetX(), this.offsetY(), this.offsetZ()) + .data(this.data()) + .force(this.force()) + .receivers(this.receivers()) + .source(this.source()); + @Nullable Location location = this.location(); + if (location != null) + particle.location(location); + + return particle.parent(this.parent()) + .override(this.override()); + } + + /** + * Returns a human-readable description of this particle, including particle type, parent shape + * (if set), and override flag. + * + * @return a string representation of this particle + */ + @Override + public String toString() { + return "Particle{" + + "particle=" + this.particle() + + (parent != null ? ", parent=" + parent : "") + + ", override=" + override + + '}'; + } + + // + + /** + * Sets the Bukkit particle type and returns {@code this} for chaining. + * + * @param particle the Bukkit particle type + * @return this {@code Particle} instance + */ + @Override + public Particle particle(org.bukkit.Particle particle) { + return (Particle) super.particle(particle); + } + + /** + * Configures this particle to be sent to all online players and returns {@code this} for + * chaining. + * + * @return this {@code Particle} instance + */ + @Override + public Particle allPlayers() { + return (Particle) super.allPlayers(); + } + + /** + * Sets the explicit list of recipient players and returns {@code this} for chaining. + * + * @param receivers the list of players who will receive this particle, or {@code null} to clear + * @return this {@code Particle} instance + */ + @Override + public Particle receivers(@Nullable List receivers) { + return (Particle) super.receivers(receivers); + } + + /** + * Sets the recipient players from a collection and returns {@code this} for chaining. + * + * @param receivers the collection of players who will receive this particle, or {@code null} to clear + * @return this {@code Particle} instance + */ + @Override + public Particle receivers(@Nullable Collection receivers) { + return (Particle) super.receivers(receivers); + } + + /** + * Sets the recipient players from a varargs array and returns {@code this} for chaining. + * + * @param receivers the players who will receive this particle, or {@code null} to clear + * @return this {@code Particle} instance + */ + @Override + public Particle receivers(Player @Nullable ... receivers) { + return (Particle) super.receivers(receivers); + } + + /** + * Sets the recipient radius, targeting all players within {@code radius} blocks of the spawn + * location, and returns {@code this} for chaining. + * + * @param radius the block radius within which players receive this particle + * @return this {@code Particle} instance + */ + @Override + public Particle receivers(int radius) { + return (Particle) super.receivers(radius); + } + + /** + * Sets the recipient radius with an optional distance sort and returns {@code this} for + * chaining. + * + * @param radius the block radius within which players receive this particle + * @param byDistance {@code true} to sort recipients by distance before sending + * @return this {@code Particle} instance + */ + @Override + public Particle receivers(int radius, boolean byDistance) { + return (Particle) super.receivers(radius, byDistance); + } + + /** + * Sets separate horizontal and vertical recipient radii and returns {@code this} for chaining. + * + * @param xzRadius the horizontal (XZ-plane) radius in blocks + * @param yRadius the vertical (Y-axis) radius in blocks + * @return this {@code Particle} instance + */ + @Override + public Particle receivers(int xzRadius, int yRadius) { + return (Particle) super.receivers(xzRadius, yRadius); + } + + /** + * Sets separate horizontal and vertical recipient radii with an optional distance sort and + * returns {@code this} for chaining. + * + * @param xzRadius the horizontal (XZ-plane) radius in blocks + * @param yRadius the vertical (Y-axis) radius in blocks + * @param byDistance {@code true} to sort recipients by distance before sending + * @return this {@code Particle} instance + */ + @Override + public Particle receivers(int xzRadius, int yRadius, boolean byDistance) { + return (Particle) super.receivers(xzRadius, yRadius, byDistance); + } + + /** + * Sets independent per-axis recipient radii and returns {@code this} for chaining. + * + * @param xRadius the radius along the X axis in blocks + * @param yRadius the radius along the Y axis in blocks + * @param zRadius the radius along the Z axis in blocks + * @return this {@code Particle} instance + */ + @Override + public Particle receivers(int xRadius, int yRadius, int zRadius) { + return (Particle) super.receivers(xRadius, yRadius, zRadius); + } + + /** + * Sets the source player (used for particle visibility checks) and returns {@code this} for + * chaining. + * + * @param source the player to treat as the particle source, or {@code null} to clear + * @return this {@code Particle} instance + */ + @Override + public Particle source(@Nullable Player source) { + return (Particle) super.source(source); + } + + /** + * Sets the spawn location from a {@link Location} and returns {@code this} for chaining. + * + * @param location the world-space location at which to spawn the particle + * @return this {@code Particle} instance + */ + @Override + public Particle location(Location location) { + return (Particle) super.location(location); + } + + /** + * Sets the spawn location from world and coordinate components and returns {@code this} for + * chaining. + * + * @param world the world in which to spawn the particle + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + * @return this {@code Particle} instance + */ + @Override + public Particle location(World world, double x, double y, double z) { + return (Particle) super.location(world, x, y, z); + } + + /** + * Sets the particle count and returns {@code this} for chaining. A count of 0 enables + * directional mode, where offset values are interpreted as velocity components. + * + * @param count the number of particles to spawn per call + * @return this {@code Particle} instance + */ + @Override + public Particle count(int count) { + return (Particle) super.count(count); + } + + /** + * Sets the offset (spread) for each axis and returns {@code this} for chaining. When count + * is 0, these values are used as directional velocity instead of random spread. + * + * @param offsetX the X-axis spread or velocity component + * @param offsetY the Y-axis spread or velocity component + * @param offsetZ the Z-axis spread or velocity component + * @return this {@code Particle} instance + */ + @Override + public Particle offset(double offsetX, double offsetY, double offsetZ) { + return (Particle) super.offset(offsetX, offsetY, offsetZ); + } + + /** + * Sets the extra data value (typically speed or intensity) and returns {@code this} for + * chaining. + * + * @param extra the extra value to pass to the particle (e.g. particle speed) + * @return this {@code Particle} instance + */ + @Override + public Particle extra(double extra) { + return (Particle) super.extra(extra); + } + + /** + * Sets whether this particle should be forced visible beyond the normal viewing distance and + * returns {@code this} for chaining. Forced particles are visible up to 256 blocks away. + * + * @param force {@code true} to bypass the normal 32-block render distance + * @return this {@code Particle} instance + */ + @Override + public Particle force(boolean force) { + return (Particle) super.force(force); + } + // + +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/ParticleRenderContext.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/ParticleRenderContext.java new file mode 100644 index 0000000..c3e0da7 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/ParticleRenderContext.java @@ -0,0 +1,94 @@ +package com.sovdee.skriptparticles.rendering; + +import com.sovdee.shapes.modifiers.PointContext; +import org.jetbrains.annotations.Nullable; +import org.joml.Quaterniond; + +/** + * Plugin-side per-point context extending shapes-lib {@link PointContext} with + * render-specific properties. Written by render modifiers, read by the renderer. + * + *

A single instance is allocated per draw call and reused across all points. + * {@link #reset()} clears all render fields between points. + */ +public class ParticleRenderContext extends PointContext { + + // ---- Set once per draw call ---- + + /** + * Shape orientation in world space, used by motion modifiers to transform the local Y axis. + */ + public @Nullable Quaterniond orientation; + + /** + * Shape scale (informational, available to modifiers). + */ + public double scale = 1.0; + + // ---- Written by render modifiers, read by the renderer ---- + + /** + * Particle type. + */ + public org.bukkit.Particle particle; + private final org.bukkit.Particle defaultParticle; + + /** + * Particle data. + */ + public Object data; + private final Object defaultData; + + /** + * Whether this point should be rendered at all. + */ + public boolean visible = true; + + /** + * Position displacement added to the world coordinates at render time. + */ + public double displacementX, displacementY, displacementZ; + + /** + * Directional motion override (used as particle velocity). Only applied when {@link #hasMotion} + * is true. + */ + public float motionX, motionY, motionZ; + + /** + * Whether {@link #motionX}/{@link #motionY}/{@link #motionZ} should override the default motion. + */ + public boolean hasMotion; + + public ParticleRenderContext(Particle particle) { + this.defaultParticle = particle.particle(); + this.particle = defaultParticle; + this.defaultData = particle.data(); + this.data = defaultData; + } + + /** + * Returns {@code true} if {@link #particle} and {@link #data} are still at their defaults. + * Used by the renderer to skip re-computing NMS options when no modifier changed them. + */ + public boolean isDefaultParticle() { + return particle == defaultParticle && data == defaultData; + } + + /** + * Resets all render fields to their defaults before each point is processed. + */ + @Override + public void reset() { + particle = defaultParticle; + data = defaultData; + visible = true; + displacementX = 0; + displacementY = 0; + displacementZ = 0; + motionX = 0; + motionY = 0; + motionZ = 0; + hasMotion = false; + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/AbstractGradientModifier.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/AbstractGradientModifier.java new file mode 100644 index 0000000..7018ff2 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/AbstractGradientModifier.java @@ -0,0 +1,117 @@ +package com.sovdee.skriptparticles.rendering.shaders; + +import com.sovdee.shapes.modifiers.EasingFunction; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.shapes.modifiers.ShapeBounds; +import com.sovdee.skriptparticles.rendering.ParticleRenderContext; +import org.bukkit.Color; +import org.bukkit.Particle.DustOptions; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Base class for gradient modifiers. Manages a list of {@link ColorStop}s and provides + * a pre-built 256-entry LUT for zero-alloc per-point colour lookup. + */ +public abstract class AbstractGradientModifier implements PointModifier { + + protected final List stops = new ArrayList<>(); + protected EasingFunction easing = EasingFunction.LINEAR; + /** + * Pre-built LUT: 256 Color objects, one per 1/255 position step. + */ + protected Color[] lut; + /** + * Pre-built DustOptions LUT, parallel to {@link #lut}. Built in {@link #buildLUT()}. + */ + protected DustOptions[] dustLut; + + protected AbstractGradientModifier() {} + + protected AbstractGradientModifier(Color from, Color to) { + stops.add(new ColorStop(0.0, from)); + stops.add(new ColorStop(1.0, to)); + } + + protected AbstractGradientModifier(List stops) { + this.stops.addAll(stops); + } + + public void addStop(ColorStop stop) { + stops.add(stop); + stops.sort(Comparator.comparingDouble(ColorStop::position)); + } + + public void addStop(double position, Color color) { + addStop(new ColorStop(position, color)); + } + + public List getStops() { + return stops; + } + + /** + * Builds the 256-entry LUT from the current stops. + * Called in {@link #prepare(ShapeBounds)}. + */ + protected void buildLUT() { + stops.sort(Comparator.comparingDouble(ColorStop::position)); + lut = new Color[256]; + dustLut = new DustOptions[256]; + for (int i = 0; i < 256; i++) { + lut[i] = ColorStop.interpolate(stops, i / 255.0); + dustLut[i] = new DustOptions(lut[i], 1.0f); + } + } + + /** + * Looks up the DustOptions LUT for {@code t ∈ [0, 1]}, applying this gradient's + * {@link EasingFunction} before the lookup. + */ + protected DustOptions dustLookup(double t) { + int idx = (int) (easing.apply(t) * 255.0 + 0.5); + if (idx < 0) idx = 0; + if (idx > 255) idx = 255; + return dustLut[idx]; + } + + public EasingFunction getEasing() { + return easing; + } + public void setEasing(EasingFunction easing) { + this.easing = easing; + } + + @Override + public void prepare(ShapeBounds bounds) { + buildLUT(); + } + + @Override + public Class contextType() { + return ParticleRenderContext.class; + } + + @Override + public int modifierHash() { + int hash = getClass().getSimpleName().hashCode(); + for (ColorStop s : stops) { + hash = 31 * hash + Double.hashCode(s.position()); + hash = 31 * hash + s.color().hashCode(); + } + hash = 31 * hash + easing.easingHash(); + return hash; + } + + protected abstract AbstractGradientModifier createInstance(); + + @Override + public AbstractGradientModifier clone() { + AbstractGradientModifier copy = createInstance(); + copy.stops.addAll(stops); // ColorStop is a record, immutable + copy.easing = easing; // EasingFunction is immutable + return copy; + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/ColorStop.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/ColorStop.java new file mode 100644 index 0000000..d4d78dd --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/ColorStop.java @@ -0,0 +1,48 @@ +package com.sovdee.skriptparticles.rendering.shaders; + +import org.bukkit.Color; + +import java.util.List; + +/** + * A color at a specific position in a gradient (position in [0, 1]). + */ +public record ColorStop(double position, Color color) { + + /** + * Interpolates between a sorted list of color stops at position {@code t}. + * Clamps t to [0, 1]. Assumes stops are sorted by position ascending. + */ + public static Color interpolate(List stops, double t) { + if (stops.isEmpty()) return Color.WHITE; + if (stops.size() == 1) return stops.getFirst().color(); + + t = Math.max(0.0, Math.min(1.0, t)); + + // Find surrounding stops + ColorStop prev = stops.getFirst(); + for (int i = 1; i < stops.size(); i++) { + ColorStop next = stops.get(i); + if (t <= next.position()) { + double range = next.position() - prev.position(); + if (range < 1e-12) return prev.color(); + double localT = (t - prev.position()) / range; + return lerp(prev.color(), next.color(), localT); + } + prev = next; + } + // t is past the last stop + return stops.getLast().color(); + } + + /** + * Linearly interpolates between two colors. + */ + private static Color lerp(Color a, Color b, double t) { + return Color.fromRGB( + (int) (a.getRed() + t * (b.getRed() - a.getRed())), + (int) (a.getGreen() + t * (b.getGreen() - a.getGreen())), + (int) (a.getBlue() + t * (b.getBlue() - a.getBlue())) + ); + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/CompositeModifier.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/CompositeModifier.java new file mode 100644 index 0000000..551907f --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/CompositeModifier.java @@ -0,0 +1,84 @@ +package com.sovdee.skriptparticles.rendering.shaders; + +import com.sovdee.shapes.modifiers.PointContext; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.shapes.modifiers.ShapeBounds; + +import java.util.ArrayList; +import java.util.List; + +/** + * Chains multiple {@link PointModifier}s. Each modifier runs on the same {@link PointContext} + * in order — later modifiers see and can further modify earlier modifiers' output. + * No temporary objects are allocated; all modifiers share the same {@code PointContext}. + */ +public class CompositeModifier implements PointModifier { + + private final List> modifiers; + + public CompositeModifier() { + this.modifiers = new ArrayList<>(); + } + + public CompositeModifier(List> modifiers) { + this.modifiers = new ArrayList<>(modifiers); + } + + @Override + public void prepare(ShapeBounds bounds) { + for (PointModifier modifier : modifiers) { + modifier.prepare(bounds); + } + } + + @Override + public void modify(Context point) { + for (PointModifier modifier : modifiers) { + modifier.modify(point); + } + } + + @Override + public Class contextType() { + Class result = PointContext.class; + for (PointModifier mod : modifiers) { + Class t = mod.contextType(); + if (result.isAssignableFrom(t)) result = t; + } + return result; + } + + @Override + public int modifierHash() { + int hash = 1; + for (PointModifier mod : modifiers) { + hash = 31 * hash + mod.modifierHash(); + } + return hash; + } + + public void addModifier(PointModifier modifier) { + modifiers.add(modifier); + } + + public boolean removeModifier(PointModifier modifier) { + return modifiers.remove(modifier); + } + + public List> getModifiers() { + return modifiers; + } + + public boolean isEmpty() { + return modifiers.isEmpty(); + } + + @Override + public CompositeModifier clone() { + List> cloned = new ArrayList<>(modifiers.size()); + for (PointModifier m : modifiers) { + cloned.add(m.clone()); + } + return new CompositeModifier<>(cloned); + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/GradientModifier.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/GradientModifier.java new file mode 100644 index 0000000..2d4bfcb --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/GradientModifier.java @@ -0,0 +1,60 @@ +package com.sovdee.skriptparticles.rendering.shaders; + +import com.sovdee.shapes.modifiers.HasInput; +import com.sovdee.shapes.modifiers.NormalizedInput; +import com.sovdee.skriptparticles.rendering.ParticleRenderContext; +import org.bukkit.Color; +import org.bukkit.Particle; + +import java.util.List; + +/** + * Colors each point by sampling a {@link NormalizedInput} and looking up the result + * in a pre-built colour gradient LUT. + */ +public class GradientModifier extends AbstractGradientModifier implements HasInput { + + private NormalizedInput input; + + public GradientModifier(Color from, Color to, NormalizedInput input) { + super(from, to); + this.input = input; + } + + public GradientModifier(List stops, NormalizedInput input) { + super(stops); + this.input = input; + } + + @Override + public void modify(ParticleRenderContext point) { + point.particle = Particle.DUST; + point.data = dustLookup(input.sample(point)); + } + + @Override + public NormalizedInput getInput() { + return input; + } + + @Override + public void setInput(NormalizedInput input) { + this.input = input; + } + + @Override + public int modifierHash() { + int hash = input.inputHash(); + for (ColorStop s : stops) { + hash = 31 * hash + Double.hashCode(s.position()); + hash = 31 * hash + s.color().hashCode(); + } + hash = 31 * hash + easing.easingHash(); + return hash; + } + + @Override + protected AbstractGradientModifier createInstance() { + return new GradientModifier(List.of(), input); + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/MotionModifier.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/MotionModifier.java new file mode 100644 index 0000000..4067573 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/rendering/shaders/MotionModifier.java @@ -0,0 +1,135 @@ +package com.sovdee.skriptparticles.rendering.shaders; + +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.shapes.modifiers.ShapeBounds; +import com.sovdee.skriptparticles.rendering.ParticleRenderContext; +import org.joml.Quaterniond; +import org.joml.Vector3d; + +/** + * Applies directional particle velocity based on the point's position relative to the shape. + * Replaces the removed {@code ParticleMotion} enum. + */ +public class MotionModifier implements PointModifier { + + public enum Mode { + /** + * Counterclockwise orbit around the shape's Y axis. + */ + COUNTERCLOCKWISE, + /** + * Clockwise orbit around the shape's Y axis. + */ + CLOCKWISE, + /** + * Velocity directed toward the origin. + */ + INWARDS, + /** + * Velocity directed away from the origin. + */ + OUTWARDS, + /** + * No velocity (zero motion). + */ + NONE + } + + private final Mode mode; + /** + * Y axis in world space after orientation transform, cached in prepare(). + */ + private float yAxisX, yAxisY, yAxisZ; + private boolean yAxisComputed = false; + + public MotionModifier(Mode mode) { + this.mode = mode; + } + + @Override + public void prepare(ShapeBounds bounds) { + // Reset so we re-compute on first ParticleRenderContext seen + yAxisComputed = false; + } + + @Override + public Class contextType() { + return ParticleRenderContext.class; + } + + @Override + public void modify(ParticleRenderContext rc) { + if (mode == Mode.NONE) return; + + // Lazily compute Y axis from orientation on first point + if (!yAxisComputed) { + if (rc.orientation != null) { + Quaterniond q = rc.orientation; + // float cast for the transform + org.joml.Quaternionf qf = new org.joml.Quaternionf( + (float) q.x, (float) q.y, (float) q.z, (float) q.w); + Vector3d y = qf.transform(new Vector3d(0, 1, 0)); + yAxisX = (float) y.x; + yAxisY = (float) y.y; + yAxisZ = (float) y.z; + } else { + yAxisX = 0; yAxisY = 1; yAxisZ = 0; + } + yAxisComputed = true; + } + + float px = (float) rc.x; + float py = (float) rc.y; + float pz = (float) rc.z; + + float mx, my, mz; + switch (mode) { + case COUNTERCLOCKWISE -> { + // cross(yAxis, point) + mx = yAxisY * pz - yAxisZ * py; + my = yAxisZ * px - yAxisX * pz; + mz = yAxisX * py - yAxisY * px; + float len = (float) Math.sqrt(mx * mx + my * my + mz * mz); + if (len > 1e-6f) { mx /= len; my /= len; mz /= len; } + } + case CLOCKWISE -> { + // -cross(yAxis, point) + mx = -(yAxisY * pz - yAxisZ * py); + my = -(yAxisZ * px - yAxisX * pz); + mz = -(yAxisX * py - yAxisY * px); + float len = (float) Math.sqrt(mx * mx + my * my + mz * mz); + if (len > 1e-6f) { mx /= len; my /= len; mz /= len; } + } + case OUTWARDS -> { + float len = (float) Math.sqrt(px * px + py * py + pz * pz); + if (len > 1e-6f) { mx = px / len; my = py / len; mz = pz / len; } + else { mx = my = mz = 0; } + } + case INWARDS -> { + float len = (float) Math.sqrt(px * px + py * py + pz * pz); + if (len > 1e-6f) { mx = -px / len; my = -py / len; mz = -pz / len; } + else { mx = my = mz = 0; } + } + default -> { return; } + } + + rc.motionX = mx; + rc.motionY = my; + rc.motionZ = mz; + rc.hasMotion = true; + } + + @Override + public int modifierHash() { + return mode.ordinal(); + } + + public Mode getMode() { + return mode; + } + + @Override + public MotionModifier clone() { + return new MotionModifier(mode); + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/DrawData.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/DrawData.java deleted file mode 100644 index 7362f9e..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/DrawData.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.sovdee.skriptparticles.shapes; - -import com.sovdee.shapes.sampling.DrawContext; -import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.particles.Particle; -import com.sovdee.skriptparticles.util.DynamicLocation; -import com.sovdee.skriptparticles.util.Quaternion; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * Plugin-side rendering metadata attached to library shapes via {@link DrawContext}. - * Holds particle, location, animation, and debug axis state. - */ -public class DrawData implements DrawContext { - - private Particle particle; - private @Nullable DynamicLocation location; - private @Nullable DynamicLocation lastLocation; - private final Quaternion lastOrientation; - private long animationDuration = 0; - private boolean drawLocalAxes = false; - private boolean drawGlobalAxes = false; - - public DrawData() { - this.particle = (Particle) new Particle(org.bukkit.Particle.FLAME).extra(0); - this.lastOrientation = Quaternion.IDENTITY.clone(); - } - - /** - * Gets the DrawData attached to a shape's PointSampler, creating and attaching one if missing. - */ - public static DrawData of(Shape shape) { - DrawContext ctx = shape.getPointSampler().getDrawContext(); - if (ctx instanceof DrawData dd) return dd; - DrawData dd = new DrawData(); - shape.getPointSampler().setDrawContext(dd); - return dd; - } - - // ---- Particle ---- - - public Particle getParticle() { return particle.clone(); } - - public Particle getParticleRaw() { return particle; } - - public void setParticle(Particle particle) { this.particle = particle; } - - // ---- Location ---- - - @Nullable - public DynamicLocation getLocation() { - if (location == null) return null; - return location.clone(); - } - - public void setLocation(DynamicLocation location) { this.location = location; } - - @Nullable - public DynamicLocation getLastLocation() { return lastLocation; } - - public void setLastLocation(@Nullable DynamicLocation lastLocation) { this.lastLocation = lastLocation; } - - // ---- Orientation ---- - - public Quaternion getLastOrientation() { return lastOrientation; } - - public void setLastOrientation(Quaternion orientation) { this.lastOrientation.set(orientation); } - - // ---- Animation ---- - - public long getAnimationDuration() { return animationDuration; } - - public void setAnimationDuration(long animationDuration) { this.animationDuration = animationDuration; } - - // ---- Axes ---- - - public boolean showLocalAxes() { return drawLocalAxes; } - - public void showLocalAxes(boolean show) { this.drawLocalAxes = show; } - - public boolean showGlobalAxes() { return drawGlobalAxes; } - - public void showGlobalAxes(boolean show) { this.drawGlobalAxes = show; } - - // ---- DrawContext ---- - - @Override - public DrawData copy() { - DrawData copy = new DrawData(); - copy.particle = this.particle.clone(); - if (this.location != null) - copy.location = this.location.clone(); - if (this.lastLocation != null) - copy.lastLocation = this.lastLocation.clone(); - copy.lastOrientation.set(this.lastOrientation); - copy.animationDuration = this.animationDuration; - copy.drawLocalAxes = this.drawLocalAxes; - copy.drawGlobalAxes = this.drawGlobalAxes; - return copy; - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/DrawManager.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/DrawManager.java deleted file mode 100644 index 7e349e4..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/DrawManager.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.sovdee.skriptparticles.shapes; - -import ch.njol.skript.Skript; -import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.particles.Particle; -import com.sovdee.skriptparticles.particles.ParticleGradient; -import com.sovdee.skriptparticles.util.DynamicLocation; -import com.sovdee.skriptparticles.util.MathUtil; -import com.sovdee.skriptparticles.util.ParticleUtil; -import com.sovdee.skriptparticles.util.Quaternion; -import com.sovdee.skriptparticles.util.VectorConversion; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.util.Vector; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joml.Quaterniond; -import org.joml.Vector3d; - -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; - -/** - * Static draw methods for rendering library shapes with plugin DrawData. - */ -public class DrawManager { - - public static void draw(Shape shape, Collection recipients) { - DrawData dd = DrawData.of(shape); - DynamicLocation location = dd.getLocation(); - if (location == null) return; - draw(shape, location, Quaternion.IDENTITY, dd.getParticleRaw(), recipients); - } - - public static void draw(Shape shape, DynamicLocation location, Collection recipients) { - draw(shape, location, Quaternion.IDENTITY, DrawData.of(shape).getParticleRaw(), recipients); - } - - public static void drawWithConsumer(Shape shape, DynamicLocation location, Consumer consumer, Collection recipients) { - consumer.accept(shape); - DrawData dd = DrawData.of(shape); - Quaterniond shapeOrientation = shape.getOrientation(); - Quaternion shapeOrientationQ = new Quaternion((float) shapeOrientation.x, (float) shapeOrientation.y, (float) shapeOrientation.z, (float) shapeOrientation.w); - draw(shape, location, shapeOrientationQ, dd.getParticleRaw(), recipients); - } - - public static void draw(Shape shape, DynamicLocation location, Quaternion baseOrientation, Particle particle, Collection recipients) { - DrawData dd = DrawData.of(shape); - - if (location.isNull()) { - DynamicLocation shapeLocation = dd.getLocation(); - if (shapeLocation == null) return; - location = shapeLocation.clone(); - } - - dd.setLastLocation(location.clone()); - Quaterniond shapeOrientation = shape.getOrientation(); - Quaternion shapeOrientationQ = new Quaternion((float) shapeOrientation.x, (float) shapeOrientation.y, (float) shapeOrientation.z, (float) shapeOrientation.w); - dd.getLastOrientation().set(baseOrientation.clone().mul(shapeOrientationQ)); - - if (!particle.override()) { - dd.getParticleRaw().parent(shape); - particle = dd.getParticleRaw(); - @Nullable ParticleGradient gradient = particle.gradient(); - if (gradient != null && gradient.isLocal()) - gradient.setOrientation(dd.getLastOrientation()); - } - - particle.receivers(recipients); - - // Get points from library shape using the last orientation - Quaterniond lastOrientationD = new Quaterniond(dd.getLastOrientation().x, dd.getLastOrientation().y, dd.getLastOrientation().z, dd.getLastOrientation().w); - Set jomlPoints = shape.getPointSampler().getPoints(shape, lastOrientationD); - Collection toDraw = VectorConversion.toBukkit(jomlPoints); - - long animationDuration = dd.getAnimationDuration(); - if (animationDuration > 0) { - int particleCount = toDraw.size(); - double millisecondsPerPoint = animationDuration / (double) particleCount; - Iterator> batchIterator = MathUtil.batch(toDraw, millisecondsPerPoint).iterator(); - Particle finalParticle = particle; - BukkitRunnable runnable = new BukkitRunnable() { - @Override - public void run() { - if (!batchIterator.hasNext()) { - this.cancel(); - return; - } - List batch = batchIterator.next(); - try { - for (Vector point : batch) { - finalParticle.spawn(point); - } - } catch (IllegalArgumentException e) { - Skript.error("Failed to spawn particle! Error: " + e.getMessage()); - } - } - }; - runnable.runTaskTimerAsynchronously(Skript.getInstance(), 0, 1); - } else { - for (Vector point : toDraw) { - try { - particle.spawn(point); - } catch (IllegalArgumentException e) { - Skript.error("Failed to spawn particle! Error: " + e.getMessage()); - return; - } - } - } - - if (dd.showLocalAxes()) { - ParticleUtil.drawAxes(location.getLocation().add(VectorConversion.toBukkit(shape.getOffset())), dd.getLastOrientation(), recipients); - } - if (dd.showGlobalAxes()) { - ParticleUtil.drawAxes(location.getLocation().add(VectorConversion.toBukkit(shape.getOffset())), Quaternion.IDENTITY, recipients); - } - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/package-info.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/package-info.java deleted file mode 100644 index e03d983..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/shapes/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -@DefaultQualifier(NonNull.class) -package com.sovdee.skriptparticles.shapes; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; \ No newline at end of file diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/SkriptParticleModule.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/SkriptParticleModule.java new file mode 100644 index 0000000..14868be --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/SkriptParticleModule.java @@ -0,0 +1,34 @@ +package com.sovdee.skriptparticles.skript; + +import com.sovdee.skriptparticles.skript.drawing.DrawingModule; +import com.sovdee.skriptparticles.skript.shaders.ShadersModule; +import com.sovdee.skriptparticles.skript.shapes.ShapesModule; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.HierarchicalAddonModule; + +import java.util.List; + +/** + * Holds all Skript syntax for skript-particle. + */ +public class SkriptParticleModule extends HierarchicalAddonModule { + + public SkriptParticleModule() { + super(); + } + + @Override + public Iterable children() { + return List.of( + new ShapesModule(this), + new DrawingModule(this), + new ShadersModule(this) + ); + } + + @Override + public String name() { + return "skript-particle"; + } + +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/DrawingModule.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/DrawingModule.java new file mode 100644 index 0000000..305403c --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/DrawingModule.java @@ -0,0 +1,41 @@ +package com.sovdee.skriptparticles.skript.drawing; + +import ch.njol.skript.registrations.EventValues; +import com.sovdee.shapes.shapes.Shape; +import com.sovdee.skriptparticles.skript.drawing.expressions.ExprDrawnShapes; +import com.sovdee.skriptparticles.skript.drawing.sections.DrawShapeEffectSection.DrawEvent; +import com.sovdee.skriptparticles.skript.drawing.sections.EffSecDrawShape; +import com.sovdee.skriptparticles.skript.drawing.sections.EffSecDrawShapeAnimation; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.HierarchicalAddonModule; +import org.skriptlang.skript.addon.SkriptAddon; + +/** + * Holds syntax related to drawing shapes with particles. + */ +public class DrawingModule extends HierarchicalAddonModule { + + public DrawingModule(AddonModule parentModule) { + super(parentModule); + } + + @Override + protected void initSelf(SkriptAddon addon) { + EventValues.registerEventValue(DrawEvent.class, Shape.class, DrawEvent::getShape, EventValues.TIME_NOW); + } + + @Override + protected void loadSelf(SkriptAddon addon) { + register(addon, + EffSecDrawShape::register, + EffSecDrawShapeAnimation::register, + ExprDrawnShapes::register + ); + } + + @Override + public String name() { + return "drawing"; + } + +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprDrawnShapes.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/expressions/ExprDrawnShapes.java similarity index 67% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprDrawnShapes.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/expressions/ExprDrawnShapes.java index 87cb82a..49c1ae0 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprDrawnShapes.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/expressions/ExprDrawnShapes.java @@ -1,17 +1,16 @@ -package com.sovdee.skriptparticles.elements.expressions; +package com.sovdee.skriptparticles.skript.drawing.expressions; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.EventValueExpression; -import ch.njol.skript.lang.ExpressionType; import com.sovdee.shapes.shapes.Shape; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxRegistry; -@Name("Drawn Shape") +@Name("Drawn/Current Shape") @Description("Returns the shape that is being drawn by the draw section.") @Examples({ "draw the shapes {_shapes::*} at player's head with radius 1:", @@ -21,8 +20,12 @@ @Since("1.0.0") public class ExprDrawnShapes extends EventValueExpression { - static { - Skript.registerExpression(ExprDrawnShapes.class, Shape.class, ExpressionType.SIMPLE, "[the] drawn shape"); + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.EXPRESSION, + infoBuilder(ExprDrawnShapes.class, Shape.class, "drawn shape") + .supplier(ExprDrawnShapes::new) + .build()); } public ExprDrawnShapes() { diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/DrawShapeEffectSection.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/DrawShapeEffectSection.java similarity index 87% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/DrawShapeEffectSection.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/DrawShapeEffectSection.java index 370bb28..8ca3e28 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/DrawShapeEffectSection.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/DrawShapeEffectSection.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.sections; +package com.sovdee.skriptparticles.skript.drawing.sections; import ch.njol.skript.Skript; import ch.njol.skript.config.SectionNode; @@ -8,7 +8,6 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.Trigger; import ch.njol.skript.lang.TriggerItem; -import ch.njol.skript.registrations.EventValues; import ch.njol.skript.util.Direction; import ch.njol.skript.util.Timespan; import ch.njol.skript.util.Timespan.TimePeriod; @@ -16,7 +15,7 @@ import ch.njol.util.Kleenean; import com.sovdee.shapes.shapes.Shape; import com.sovdee.skriptparticles.SkriptParticle; -import com.sovdee.skriptparticles.shapes.DrawManager; +import com.sovdee.skriptparticles.rendering.DrawManager; import com.sovdee.skriptparticles.util.DynamicLocation; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -25,8 +24,8 @@ import org.bukkit.event.Event; import org.bukkit.event.HandlerList; import org.bukkit.scheduler.BukkitRunnable; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; @@ -38,25 +37,20 @@ public abstract class DrawShapeEffectSection extends EffectSection { public static final Timespan ONE_TICK = new Timespan(TimePeriod.TICK, 1); - static { - EventValues.registerEventValue(DrawEvent.class, Shape.class, DrawEvent::getShape, EventValues.TIME_NOW); - } - protected Expression shapes; - @Nullable - protected Expression directions; - @Nullable - protected Expression locations; - @Nullable - protected Expression players; - @Nullable - private Trigger trigger; + protected @Nullable Expression directions; + protected @Nullable Expression locations; + protected @Nullable Expression players; + private @Nullable Trigger trigger; protected boolean useShapeLocation; protected boolean sync; @Override - public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, @Nullable SectionNode sectionNode, @Nullable List list) { + public boolean init( + Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, + @Nullable SectionNode sectionNode, @Nullable List list + ) { if (hasSection()) { AtomicBoolean delayed = new AtomicBoolean(false); Runnable afterLoading = () -> delayed.set(!getParser().getHasDelayBefore().isFalse()); @@ -70,7 +64,8 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is return init(expressions, matchedPattern, isDelayed, parseResult, hasSection()); } - public boolean init(@Nullable Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, boolean hasSection) { + @SuppressWarnings("unchecked") + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, boolean hasSection) { shapes = (Expression) expressions[0]; if (expressions[2] != null) { @@ -94,6 +89,7 @@ protected TriggerItem walk(Event event) { Delay.addDelayedEvent(event); + // determine recipients Collection recipients = new ArrayList<>(); if (players != null) { recipients.addAll(List.of(players.getArray(event))); @@ -101,8 +97,7 @@ protected TriggerItem walk(Event event) { recipients.addAll(Bukkit.getOnlinePlayers()); } - @Nullable Object localVars = Variables.copyLocalVariables(event); - + // set up consumer to do the pre-drawing execution @Nullable Consumer consumer; if (trigger != null) { consumer = shape -> { @@ -113,6 +108,7 @@ protected TriggerItem walk(Event event) { consumer = null; } + // determine locations to draw at List locations = new ArrayList<>(); @Nullable Direction direction = null; if (!useShapeLocation) { @@ -130,6 +126,8 @@ protected TriggerItem walk(Event event) { locations.add(new DynamicLocation()); } + // run the code and draw the shapes + // async drawing runs all the code sync prior to dispatching the drawing thread if (sync) { executeSync(event, locations, consumer, recipients); } else { @@ -149,16 +147,6 @@ protected TriggerItem walk(Event event) { return getNext(); } - protected void setupAsync(Event event, Collection locations, Collection shapes, Collection recipients) { - BukkitRunnable runnable = new BukkitRunnable() { - @Override - public void run() { - executeAsync(locations, shapes, recipients); - } - }; - runnable.runTaskAsynchronously(Skript.getInstance()); - } - protected void executeSync(Event event, Collection locations, @Nullable Consumer consumer, Collection recipients) { try { for (DynamicLocation dynamicLocation : locations) { @@ -180,6 +168,16 @@ protected void executeSync(Event event, Collection locations, @ } } + protected void setupAsync(Event event, Collection locations, Collection shapes, Collection recipients) { + BukkitRunnable runnable = new BukkitRunnable() { + @Override + public void run() { + executeAsync(locations, shapes, recipients); + } + }; + runnable.runTaskAsynchronously(Skript.getInstance()); + } + protected void executeAsync(Collection locations, Collection shapes, Collection recipients) { try { for (DynamicLocation dynamicLocation : locations) { @@ -208,7 +206,7 @@ public Shape getShape() { } @Override - @NonNull + @NotNull public HandlerList getHandlers() { throw new IllegalStateException(); } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/EffSecDrawShape.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/EffSecDrawShape.java similarity index 80% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/EffSecDrawShape.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/EffSecDrawShape.java index c3afaba..c220d34 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/EffSecDrawShape.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/EffSecDrawShape.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.sections; +package com.sovdee.skriptparticles.skript.drawing.sections; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; @@ -7,17 +7,18 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.registrations.EventValues; import ch.njol.skript.util.Timespan; import ch.njol.skript.util.Timespan.TimePeriod; import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.shapes.shapes.Shape; import com.sovdee.skriptparticles.util.DynamicLocation; import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.scheduler.BukkitRunnable; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.concurrent.atomic.AtomicLong; @@ -53,21 +54,22 @@ @Since("1.0.0") public class EffSecDrawShape extends DrawShapeEffectSection { - static { - Skript.registerSection(EffSecDrawShape.class, - "[sync:sync[hronously]] draw [the] shape[s] [of] %shapes% [%-directions% %-locations/entities%] [to %-players%]", - "draw [the] shape[s] [of] %shapes% [%-directions% %-locations/entities%] [to %-players%] (duration:for) [duration] %timespan% [with (delay|refresh [rate]) [of] %-timespan%]" - ); - EventValues.registerEventValue(EffSecDrawShape.DrawEvent.class, Shape.class, DrawEvent::getShape, EventValues.TIME_NOW); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.SECTION, SyntaxInfo.builder(EffSecDrawShape.class) + .supplier(EffSecDrawShape::new) + .addPatterns( + "[sync:sync[hronously]] draw [the] shape[s] [of] %shapes% [%-directions% %-locations/entities%] [to %-players%]", + "draw [the] shape[s] [of] %shapes% [%-directions% %-locations/entities%] [to %-players%] (duration:for) [duration] %timespan% [with (delay|refresh [rate]) [of] %-timespan%]" + ) + .build()); } - @Nullable - private Expression duration; - @Nullable - private Expression delay; + private @Nullable Expression duration; + private @Nullable Expression delay; @Override - public boolean init(@Nullable Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, boolean hasSection) { + @SuppressWarnings("unchecked") + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, boolean hasSection) { if (parseResult.hasTag("duration")) { duration = (Expression) expressions[4]; delay = (Expression) expressions[5]; @@ -103,7 +105,7 @@ public void run() { } @Override - @NonNull + @NotNull public String toString(@Nullable Event event, boolean b) { return "draw shape " + shapes.toString(event, b) + (locations != null ? " at " + locations.toString(event, b) : "") + " for " + (players == null ? "all players" : players.toString(event, b)); } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/EffSecDrawShapeAnimation.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/EffSecDrawShapeAnimation.java similarity index 74% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/EffSecDrawShapeAnimation.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/EffSecDrawShapeAnimation.java index d65dbf1..de26e9d 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/sections/EffSecDrawShapeAnimation.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/drawing/sections/EffSecDrawShapeAnimation.java @@ -1,6 +1,5 @@ -package com.sovdee.skriptparticles.elements.sections; +package com.sovdee.skriptparticles.skript.drawing.sections; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; @@ -10,13 +9,15 @@ import ch.njol.skript.util.Timespan; import ch.njol.skript.util.Timespan.TimePeriod; import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.DynamicLocation; import org.bukkit.entity.Player; import org.bukkit.event.Event; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.function.Consumer; @@ -29,15 +30,17 @@ @Since("1.2.0") public class EffSecDrawShapeAnimation extends DrawShapeEffectSection { - static { - Skript.registerSection(EffSecDrawShapeAnimation.class, - "draw [an] (animation [of] [the]|animated) shape[s] [of] %shapes% [%-directions% %-locations/entities%] [to %-players%] over %timespan%" - ); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.SECTION, SyntaxInfo.builder(EffSecDrawShapeAnimation.class) + .supplier(EffSecDrawShapeAnimation::new) + .addPatterns("draw [an] (animation [of] [the]|animated) shape[s] [of] %shapes% [%-directions% %-locations/entities%] [to %-players%] over %timespan%") + .build()); } private Expression duration; @Override + @SuppressWarnings("unchecked") public boolean init(@Nullable Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, boolean hasSection) { duration = (Expression) expressions[4]; return super.init(expressions, matchedPattern, isDelayed, parseResult, hasSection); @@ -62,7 +65,7 @@ protected void setupAsync(Event event, Collection locations, Co } @Override - @NonNull + @NotNull public String toString(@Nullable Event event, boolean debug) { return "draw an animation of the shape of " + shapes.toString(event, debug) + " at " + (locations != null ? locations.toString(event, debug) : "shape's location") + " for " + (players == null ? "all players" : players.toString(event, debug) + " over " + duration.toString(event, debug)); diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/ShadersModule.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/ShadersModule.java new file mode 100644 index 0000000..1be6f82 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/ShadersModule.java @@ -0,0 +1,93 @@ +package com.sovdee.skriptparticles.skript.shaders; + +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.Parser; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.registrations.Classes; +import com.sovdee.shapes.modifiers.EasingFunction; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.skriptparticles.skript.shaders.effects.EffAddColorStop; +import com.sovdee.skriptparticles.skript.shaders.expressions.ExprEasing; +import com.sovdee.skriptparticles.skript.shaders.expressions.ExprGradientModifier; +import com.sovdee.skriptparticles.skript.shaders.expressions.ExprModifier; +import com.sovdee.skriptparticles.skript.shaders.expressions.ExprMotionModifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.HierarchicalAddonModule; +import org.skriptlang.skript.addon.SkriptAddon; + +public class ShadersModule extends HierarchicalAddonModule { + + public ShadersModule(AddonModule parentModule) { + super(parentModule); + } + + @Override + protected void initSelf(SkriptAddon addon) { + Classes.registerClass(new ClassInfo<>(PointModifier.class, "pointmodifier") + .user("point ?modifiers?") + .name("Point Modifier") + .description("A modifier applied to shape points. Geometry modifiers (taper, twist, wave) transform positions and are cached. Render modifiers (gradients, motion) run per-frame and affect color, motion, or visibility.") + .parser(new Parser<>() { + @Override + public @Nullable PointModifier parse(String s, ParseContext context) { return null; } + @Override + public boolean canParse(ParseContext context) { return false; } + @Override + public @NotNull String toString(PointModifier o, int flags) { return o.toString(); } + @Override + public @NotNull String toVariableNameString(PointModifier o) { return "modifier:" + o.getClass().getSimpleName(); } + }) + ); + + Classes.registerClass(new ClassInfo<>(EasingFunction.class, "easing") + .user("easings?") + .name("Easing Function") + .description("A curve that reshapes the normalised input value of modifiers and gradients.", + "Supported curves: linear, quadratic ease in/out/in-out, cubic ease in/out/in-out,", + "sine ease in/out/in-out, power ease with a custom exponent, and custom Skript functions.") + .parser(new Parser<>() { + @Override + public @Nullable EasingFunction parse(String s, ParseContext context) { return null; } + @Override + public boolean canParse(ParseContext context) { return false; } + @Override + public @NotNull String toString(EasingFunction o, int flags) { + return switch (o) { + case EasingFunction.PowerEasing p -> switch (p.mode()) { + case IN -> "ease in (power " + p.exponent() + ")"; + case OUT -> "ease out (power " + p.exponent() + ")"; + case IN_OUT -> "ease in-out (power " + p.exponent() + ")"; + }; + case EasingFunction.SineEasing s -> switch (s.mode()) { + case IN -> "sine ease in"; + case OUT -> "sine ease out"; + case IN_OUT -> "sine ease in-out"; + }; + default -> "easing"; + }; + } + @Override + public @NotNull String toVariableNameString(EasingFunction o) { return "easing:" + o.easingHash(); } + }) + ); + } + + @Override + protected void loadSelf(SkriptAddon addon) { + register(addon, + ExprGradientModifier::register, + ExprMotionModifier::register, + ExprModifier::register, + ExprEasing::register, + EffAddColorStop::register + ); + } + + @Override + public String name() { + return "shaders"; + } + +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/effects/EffAddColorStop.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/effects/EffAddColorStop.java new file mode 100644 index 0000000..216f315 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/effects/EffAddColorStop.java @@ -0,0 +1,87 @@ +package com.sovdee.skriptparticles.skript.shaders.effects; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.skriptparticles.rendering.shaders.AbstractGradientModifier; +import org.bukkit.Color; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +/** + * Skript effect that adds a color stop at a specific normalized position to a gradient modifier. + * See {@link #init}, {@link #execute}, and {@link #toString} for behaviour details. + * Documented from the Skript side via {@code @Name}, {@code @Description}, {@code @Examples}, and {@code @Since}. + */ +@Name("Add Gradient Color Stop") +@Description({ + "Adds a color stop at a specific position to a gradient modifier.", + "Position is a number between 0 and 1.", + "This effect is primarily used inside a section body when building a gradient with custom stop positions.", +}) +@Examples({ + "add color stop at 0 colored red to {_gradient}", + "add stop at 0.5 colored yellow to {_gradient}", + "add color stop at 1 colored blue to {_gradient}", +}) +@Since("2.0.0") +public class EffAddColorStop extends Effect { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EFFECT, SyntaxInfo.builder(EffAddColorStop.class) + .supplier(EffAddColorStop::new) + .addPatterns("add [color] stop at %number% color[ed] %color% to %pointmodifier%") + .build()); + } + + private Expression position; + private Expression color; + private Expression modifier; + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + this.position = (Expression) exprs[0]; + this.color = (Expression) exprs[1]; + this.modifier = (Expression) exprs[2]; + return true; + } + + /** + * Adds the color stop to the modifier only if it is an {@link AbstractGradientModifier}; + * silently does nothing otherwise. + */ + @Override + protected void execute(Event event) { + Number pos = position.getSingle(event); + Color col = color.getSingle(event); + PointModifier mod = modifier.getSingle(event); + + if (pos == null || col == null || mod == null) return; + + if (mod instanceof AbstractGradientModifier gradientModifier) { + gradientModifier.addStop(pos.doubleValue(), col); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString(@Nullable Event event, boolean debug) { + return "add color stop at " + position.toString(event, debug) + + " colored " + color.toString(event, debug) + + " to " + modifier.toString(event, debug); + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprEasing.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprEasing.java new file mode 100644 index 0000000..51f4c4c --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprEasing.java @@ -0,0 +1,169 @@ +package com.sovdee.skriptparticles.skript.shaders.expressions; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import ch.njol.skript.lang.function.FunctionEvent; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import com.sovdee.shapes.modifiers.EasingFunction; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +@Name("Easing Function") +@Description({ + "Creates an easing function that reshapes the normalised input of modifiers and gradients.", + "Built-in curves: linear, quadratic, cubic, and sine — each available as ease in, ease out, or ease in-out.", + "Power curves accept a custom exponent (e.g. 4 for quartic, 0.5 for square-root).", + "Custom: provide the name of a Skript function that takes one number and returns one number.", + " The function should be a pure math transformation; it runs on whatever thread the modifier runs on.", +}) +@Examples({ + "set {_e} to linear easing", + "set {_e} to ease in quadratic", + "set {_e} to ease out cubic", + "set {_e} to ease in-out sine", + "set {_e} to power ease in with exponent 4", + "set {_e} to power ease out with exponent 0.5", + "add taper from 0 to 1 eased with ease in quadratic to modifiers of {_shape}", + "add gradient from red to blue along y eased with ease in-out sine to modifiers of {_shape}", + "function myEase(t: number) :: number:", + " return {_t} ^ 3", + "set {_e} to custom easing from function \"myEase\"", +}) +@Since("2.0.0") +public class ExprEasing extends SimpleExpression { + + // Pattern 0: linear + // Patterns 1-3: ease in/out/in-out [(quadratic|cubic|sine)] + // mark: 0=quadratic (default), 1=quadratic explicit, 2=cubic, 3=sine + // Pattern 4: power ease (in|out|in-out) with exponent %number% + // mark: 0=in, 1=out, 2=in-out + // Pattern 5: custom Skript function name + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprEasing.class, EasingFunction.class) + .supplier(ExprEasing::new) + .addPatterns( + "linear [easing]", + "(ease in|in ease) [(0:quadratic|1:quadratic|2:cubic|3:sine)] [easing]", + "(ease out|out ease) [(0:quadratic|1:quadratic|2:cubic|3:sine)] [easing]", + "(ease in[- ]out|in[- ]out ease) [(0:quadratic|1:quadratic|2:cubic|3:sine)] [easing]", + "[a] power ease (0:in|1:out|2:in[- ]out) with exponent %number%", + "[a] custom easing [from [function]] %string%" + ) + .build()); + } + + private int pattern; + private int parseMark; + private @Nullable Expression exponent; // pattern 4 + private @Nullable Expression funcName; // pattern 5 + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + this.pattern = matchedPattern; + this.parseMark = parseResult.mark; + if (matchedPattern == 4) this.exponent = (Expression) exprs[0]; + if (matchedPattern == 5) this.funcName = (Expression) exprs[0]; + return true; + } + + @Override + protected EasingFunction @Nullable [] get(Event event) { + return new EasingFunction[]{ build(event) }; + } + + private EasingFunction build(Event event) { + return switch (pattern) { + case 1 -> curveFromMark(parseMark, EasingFunction.Mode.IN); + case 2 -> curveFromMark(parseMark, EasingFunction.Mode.OUT); + case 3 -> curveFromMark(parseMark, EasingFunction.Mode.IN_OUT); + case 4 -> { + Number n = exponent != null ? exponent.getSingle(event) : null; + double exp = (n != null) ? n.doubleValue() : 2.0; + EasingFunction.Mode mode = switch (parseMark) { + case 1 -> EasingFunction.Mode.OUT; + case 2 -> EasingFunction.Mode.IN_OUT; + default -> EasingFunction.Mode.IN; + }; + yield new EasingFunction.PowerEasing(exp, mode); + } + case 5 -> { + String name = funcName != null ? funcName.getSingle(event) : null; + yield name != null ? new SkriptFunctionEasing(name) : EasingFunction.LINEAR; + } + default -> EasingFunction.LINEAR; + }; + } + + /** + * Selects the right built-in curve for the parse mark (0/1=quad, 2=cubic, 3=sine). + */ + private static EasingFunction curveFromMark(int mark, EasingFunction.Mode mode) { + return switch (mark) { + case 2 -> new EasingFunction.PowerEasing(3.0, mode); // cubic + case 3 -> new EasingFunction.SineEasing(mode); // sine + default -> new EasingFunction.PowerEasing(2.0, mode); // quadratic (0 or 1) + }; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return EasingFunction.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return switch (pattern) { + case 0 -> "linear easing"; + case 1 -> "ease in"; + case 2 -> "ease out"; + case 3 -> "ease in-out"; + case 4 -> "power ease"; + case 5 -> "custom easing"; + default -> "easing"; + }; + } + + // ------------------------------------------------------------------------- + // Custom Skript-function-backed easing + // ------------------------------------------------------------------------- + + /** + * Calls a user-defined Skript function {@code f(t: number) :: number} as an easing curve. + * The function should be pure math — it runs on whatever thread the modifier does. + */ + private record SkriptFunctionEasing(String name) implements EasingFunction { + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public double apply(double t) { + var fn = Functions.getGlobalFunction(name); + if (fn == null) return t; + try { + FunctionEvent fe = new FunctionEvent(null); + Object[] result = fn.execute(fe, new Object[][]{{t}}); + if (result != null && result.length > 0 && result[0] instanceof Number n) + return n.doubleValue(); + } catch (Exception ignored) {} + return t; + } + + @Override + public int easingHash() { + return name.hashCode(); + } + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprGradientModifier.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprGradientModifier.java new file mode 100644 index 0000000..e96fcf7 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprGradientModifier.java @@ -0,0 +1,171 @@ +package com.sovdee.skriptparticles.skript.shaders.expressions; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.util.Color; +import ch.njol.util.Kleenean; +import com.sovdee.shapes.modifiers.EasingFunction; +import com.sovdee.shapes.modifiers.NormalizedInput; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.shapes.modifiers.StandardInput; +import com.sovdee.skriptparticles.rendering.ParticleRenderContext; +import com.sovdee.skriptparticles.rendering.shaders.AbstractGradientModifier; +import com.sovdee.skriptparticles.rendering.shaders.ColorStop; +import com.sovdee.skriptparticles.rendering.shaders.GradientModifier; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +@Name("Gradient Modifier Constructor") +@Description({ + "Creates a gradient modifier for coloring shape points.", + "Two-color form: interpolates from the first color to the second.", + "Multi-color form: evenly spaces the provided colors across [0, 1].", + "Axes: x, y (default), z, or t (normalised draw order) for axis-aligned gradients.", + "Radial: gradient based on XZ distance from center.", + "Angular: gradient based on angle around the Y axis.", + "Spherical: gradient based on 3D distance from center.", +}) +@Examples({ + "add gradient from red to blue along the y axis to modifiers of {_shape}", + "add gradient from red, yellow, green, blue along the y axis to modifiers of {_shape}", + "add radial gradient from white to black to modifiers of {_shape}", + "add angular gradient from red to blue to modifiers of {_shape}", + "add spherical gradient from yellow to orange to modifiers of {_shape}", + "set modifiers of {_shape} to gradient from red to blue along the y axis", +}) +@Since("2.0.0") +public class ExprGradientModifier extends SimpleExpression { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprGradientModifier.class, PointModifier.class) + .supplier(ExprGradientModifier::new) + .addPatterns( + // Pattern 0: two-color axis gradient + "[a] [color] gradient from %color% to %color% along [the] (0¦x|1¦y|2¦z|3¦t) [axis] [eased with %-easing%]", + // Pattern 1: multi-color axis gradient (evenly spaced) + "[a] [color] gradient from %colors% along [the] (0¦x|1¦y|2¦z|3¦t) [axis] [eased with %-easing%]", + // Pattern 2: radial gradient (two colors) + "[a] radial [color] gradient from %color% to %color% [eased with %-easing%]", + // Pattern 3: angular gradient (two colors) + "[a] angular [color] gradient from %color% to %color% [eased with %-easing%]", + // Pattern 4: spherical gradient (two colors) + "[a] spherical [color] gradient from %color% to %color% [eased with %-easing%]" + ) + .build()); + } + + private int pattern; + private int parseMark; + private Expression color1, color2; + private Expression colors; // for multi-color + private @Nullable Expression easing; + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + this.pattern = matchedPattern; + this.parseMark = parseResult.mark; + if (matchedPattern == 1) { + this.colors = (Expression) exprs[0]; + this.easing = (Expression) exprs[1]; + } else { + this.color1 = (Expression) exprs[0]; + this.color2 = (Expression) exprs[1]; + this.easing = (Expression) exprs[2]; + } + return true; + } + + @Override + protected PointModifier @Nullable [] get(Event event) { + AbstractGradientModifier mod = switch (pattern) { + case 0 -> { // two-color axis gradient + Color c1 = color1.getSingle(event); + Color c2 = color2.getSingle(event); + if (c1 == null || c2 == null) yield null; + yield new GradientModifier(c1.asBukkitColor(), c2.asBukkitColor(), axisFromMark(parseMark)); + } + case 1 -> { // multi-color axis gradient (evenly spaced) + Color[] cs = colors.getArray(event); + if (cs == null || cs.length == 0) yield null; + yield new GradientModifier(evenlySpaced(cs), axisFromMark(parseMark)); + } + case 2 -> { // radial + Color c1 = color1.getSingle(event); + Color c2 = color2.getSingle(event); + if (c1 == null || c2 == null) yield null; + yield new GradientModifier(c1.asBukkitColor(), c2.asBukkitColor(), StandardInput.RADIUS); + } + case 3 -> { // angular + Color c1 = color1.getSingle(event); + Color c2 = color2.getSingle(event); + if (c1 == null || c2 == null) yield null; + yield new GradientModifier(c1.asBukkitColor(), c2.asBukkitColor(), StandardInput.ANGLE); + } + case 4 -> { // spherical + Color c1 = color1.getSingle(event); + Color c2 = color2.getSingle(event); + if (c1 == null || c2 == null) yield null; + yield new GradientModifier(c1.asBukkitColor(), c2.asBukkitColor(), StandardInput.SPHERICAL); + } + default -> null; + }; + if (mod == null) return null; + if (easing != null) { EasingFunction ef = easing.getSingle(event); if (ef != null) mod.setEasing(ef); } + return new PointModifier[]{mod}; + } + + private static NormalizedInput axisFromMark(int mark) { + return switch (mark) { + case 0 -> StandardInput.X; + case 2 -> StandardInput.Z; + case 3 -> StandardInput.T; + default -> StandardInput.Y; + }; + } + + private static List evenlySpaced(Color[] colors) { + List stops = new ArrayList<>(colors.length); + if (colors.length == 1) { + stops.add(new ColorStop(0.0, colors[0].asBukkitColor())); + stops.add(new ColorStop(1.0, colors[0].asBukkitColor())); + return stops; + } + for (int i = 0; i < colors.length; i++) { + stops.add(new ColorStop((double) i / (colors.length - 1), colors[i].asBukkitColor())); + } + return stops; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return PointModifier.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return switch (pattern) { + case 0 -> "gradient from " + color1.toString(event, debug) + " to " + color2.toString(event, debug) + " along axis"; + case 1 -> "gradient from colors along axis"; + case 2 -> "radial gradient"; + case 3 -> "angular gradient"; + case 4 -> "spherical gradient"; + default -> "gradient modifier"; + }; + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprModifier.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprModifier.java new file mode 100644 index 0000000..1d61b86 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprModifier.java @@ -0,0 +1,201 @@ +package com.sovdee.skriptparticles.skript.shaders.expressions; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import com.sovdee.shapes.modifiers.EasingFunction; +import com.sovdee.shapes.modifiers.NormalizedInput; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.shapes.modifiers.RotationPlane; +import com.sovdee.shapes.modifiers.ScaleAxes; +import com.sovdee.shapes.modifiers.ScalingModifier; +import com.sovdee.shapes.modifiers.SpatialAxis; +import com.sovdee.shapes.modifiers.StandardInput; +import com.sovdee.shapes.modifiers.TwistModifier; +import com.sovdee.shapes.modifiers.WaveModifier; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +@Name("Point Modifier Constructor") +@Description({ + "Creates a point modifier that transforms shape geometry before drawing.", + "- Scale: scales the specified axes by lerp(start, end, input). Default: scales XZ by Y input.", + " Inputs: y (default), x, z, t, radius, spherical, angle.", + " Scale axes: xz (default), xy, yz, x, y, z, xyz.", + "- Twist: rotates the specified plane by angle * input. Default: XZ plane driven by Y input.", + " Inputs: y (default), x, z, t, radius, spherical, angle.", + " Planes: xz (default), xy, yz.", + "- Wave: displaces an output axis by amplitude * sin(2π * frequency * input + phase).", + " Output axis: x, y (default), or z. Input: x, y, z, t (default), radius, spherical, angle.", + " frequency=1 means one full sine cycle over the full input range.", + " When the displacing clause is omitted, defaults to displacing y by t.", +}) +@Examples({ + "set {_m} to a scale from 1 to 0", + "add scale from 1 to 0 along the y axis to modifiers of {_shape}", + "add scale from 1 to 0 driven by radius scaling xyz to modifiers of {_shape}", + "add a twist of 360 degrees to modifiers of {_shape}", + "add a twist of 180 degrees in the xz plane driven by radius to modifiers of {_shape}", + "add a wave with amplitude 0.5 and frequency 2 displacing y by t to modifiers of {_shape}", + "add a wave with amplitude 0.3 and frequency 4 displacing y by x to modifiers of {_shape}", + "add a wave with amplitude 0.5 and frequency 1 displacing x by radius to modifiers of {_shape}", +}) +@Since("2.0.0") +public class ExprModifier extends SimpleExpression { + + // Scale parse mark bit layout: + // bits 0-2 (& 7) → input: 0=Y, 1=X, 2=Z, 3=T, 4=RADIUS, 5=SPHERICAL, 6=ANGLE + // bits 3-5 (>> 3) → scale axes: 0=XZ (default), 1=X, 2=Y, 3=Z, 4=XY, 5=YZ, 6=XYZ + + // Twist parse mark bit layout: + // bit 0 → unit: 0=degrees (default), 1=radians + // bits 1-3 → input: 0=Y (default), 1=X, 2=Z, 3=T, 4=RADIUS, 5=SPHERICAL, 6=ANGLE (stored as mark>>1 & 7) + // bits 4-5 → plane: 0=XZ (default), 1=XY, 2=YZ (stored as mark>>4) + + // Wave parse mark bit layout: + // bits 0-1 (& 3) → output axis: 0=Y (default), 1=X, 2=Z + // bits 2-4 (>> 2) → input: 0=T (default), 1=X, 2=Y, 3=Z, 4=RADIUS, 5=SPHERICAL, 6=ANGLE + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprModifier.class, PointModifier.class) + .supplier(ExprModifier::new) + .addPatterns( + "[a] scale from %number% to %number% [along [the] (0¦y|1¦x|2¦z|3¦t|4¦radius|5¦spherical|6¦angle) [axis]|driven by (0¦y|1¦x|2¦z|3¦t|4¦radius|5¦spherical|6¦angle)] [scaling (0¦xz|8¦x|16¦y|24¦z|32¦xy|40¦yz|48¦xyz)] [eased with %-easing%]", + "[a] twist of %number% (0¦degrees|1¦radians) [in [the] (0¦xz|16¦xy|32¦yz) plane] [driven by (0¦y|2¦x|4¦z|6¦t|8¦radius|10¦spherical|12¦angle)] [eased with %-easing%]", + "[a] wave with amplitude %number% [and] frequency %number% [displacing [the] (1¦x|0¦y|2¦z) [axis] [by [the] (4¦x|8¦y|12¦z|0¦t|16¦radius|20¦spherical|24¦angle) [axis]]]" + ) + .build()); + } + + private int pattern; + private int parseMark; + private Expression arg1, arg2; + private @Nullable Expression easing; + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + this.pattern = matchedPattern; + this.parseMark = parseResult.mark; + this.arg1 = (Expression) exprs[0]; + if (matchedPattern == 0) { + this.arg2 = (Expression) exprs[1]; + this.easing = (Expression) exprs[2]; + } else if (matchedPattern == 1) { + this.easing = (Expression) exprs[1]; + } else { + if (exprs.length > 1) this.arg2 = (Expression) exprs[1]; + } + return true; + } + + @Override + protected PointModifier @Nullable [] get(Event event) { + Number n1 = arg1.getSingle(event); + if (n1 == null) return null; + + return switch (pattern) { + case 0 -> { // scale + Number n2 = arg2.getSingle(event); + if (n2 == null) yield null; + NormalizedInput input = inputFromMark(parseMark & 7); + ScaleAxes axes = scaleAxesFromMark((parseMark >> 3) & 7); + ScalingModifier mod = new ScalingModifier(n1.doubleValue(), n2.doubleValue(), input, axes); + if (easing != null) { EasingFunction ef = easing.getSingle(event); if (ef != null) mod.setEasing(ef); } + yield new PointModifier[]{mod}; + } + case 1 -> { // twist + double angle = n1.doubleValue(); + if ((parseMark & 1) == 0) angle = Math.toRadians(angle); // degrees + NormalizedInput input = inputFromMark((parseMark >> 1) & 7); + RotationPlane plane = rotationPlaneFromMark(parseMark >> 4); + TwistModifier mod = new TwistModifier(angle, input, plane); + if (easing != null) { EasingFunction ef = easing.getSingle(event); if (ef != null) mod.setEasing(ef); } + yield new PointModifier[]{mod}; + } + case 2 -> { // wave + Number n2 = arg2.getSingle(event); + if (n2 == null) yield null; + SpatialAxis outputAxis = switch (parseMark & 3) { + case 1 -> SpatialAxis.X; + case 2 -> SpatialAxis.Z; + default -> SpatialAxis.Y; + }; + NormalizedInput input = waveInputFromMark(parseMark >> 2); + yield new PointModifier[]{new WaveModifier(n1.doubleValue(), n2.doubleValue(), 0.0, input, outputAxis)}; + } + default -> null; + }; + } + + private static NormalizedInput inputFromMark(int mark) { + return switch (mark) { + case 1 -> StandardInput.X; + case 2 -> StandardInput.Z; + case 3 -> StandardInput.T; + case 4 -> StandardInput.RADIUS; + case 5 -> StandardInput.SPHERICAL; + case 6 -> StandardInput.ANGLE; + default -> StandardInput.Y; + }; + } + + private static NormalizedInput waveInputFromMark(int mark) { + return switch (mark) { + case 1 -> StandardInput.X; + case 2 -> StandardInput.Y; + case 3 -> StandardInput.Z; + case 4 -> StandardInput.RADIUS; + case 5 -> StandardInput.SPHERICAL; + case 6 -> StandardInput.ANGLE; + default -> StandardInput.T; + }; + } + + private static ScaleAxes scaleAxesFromMark(int mark) { + return switch (mark) { + case 1 -> ScaleAxes.X; + case 2 -> ScaleAxes.Y; + case 3 -> ScaleAxes.Z; + case 4 -> ScaleAxes.XY; + case 5 -> ScaleAxes.YZ; + case 6 -> ScaleAxes.XYZ; + default -> ScaleAxes.XZ; + }; + } + + private static RotationPlane rotationPlaneFromMark(int mark) { + return switch (mark) { + case 1 -> RotationPlane.XY; + case 2 -> RotationPlane.YZ; + default -> RotationPlane.XZ; + }; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return PointModifier.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return switch (pattern) { + case 0 -> "scale from " + arg1.toString(event, debug) + " to " + arg2.toString(event, debug); + case 1 -> "twist of " + arg1.toString(event, debug) + ((parseMark & 1) == 0 ? " degrees" : " radians"); + case 2 -> "wave with amplitude " + arg1.toString(event, debug) + " frequency " + arg2.toString(event, debug); + default -> "modifier"; + }; + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprMotionModifier.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprMotionModifier.java new file mode 100644 index 0000000..5d27e86 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shaders/expressions/ExprMotionModifier.java @@ -0,0 +1,73 @@ +package com.sovdee.skriptparticles.skript.shaders.expressions; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.skriptparticles.rendering.shaders.MotionModifier; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +@Name("Motion Modifier Constructor") +@Description({ + "Creates a motion (velocity) modifier for particles.", + "Modes: clockwise, counterclockwise, inwards, outwards, none.", + "The velocity is normalized and based on each point's position relative to the shape origin.", +}) +@Examples({ + "add motion modifier counterclockwise to modifiers of {_shape}", + "add motion modifier inwards to modifiers of {_shape}", + "set modifiers of {_shape} to motion modifier clockwise", +}) +@Since("2.0.0") +public class ExprMotionModifier extends SimpleExpression { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprMotionModifier.class, PointModifier.class) + .supplier(ExprMotionModifier::new) + .addPatterns("[a] motion modifier (0¦clockwise|1¦counterclockwise|2¦inwards|3¦outwards|4¦none)") + .priority(SyntaxInfo.SIMPLE) + .build()); + } + + private MotionModifier.Mode mode; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + this.mode = switch (parseResult.mark) { + case 0 -> MotionModifier.Mode.CLOCKWISE; + case 1 -> MotionModifier.Mode.COUNTERCLOCKWISE; + case 2 -> MotionModifier.Mode.INWARDS; + case 3 -> MotionModifier.Mode.OUTWARDS; + default -> MotionModifier.Mode.NONE; + }; + return true; + } + + @Override + protected PointModifier @Nullable [] get(Event event) { + return new PointModifier[]{new MotionModifier(mode)}; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return PointModifier.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "motion modifier " + mode.name().toLowerCase(); + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/ShapesModule.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/ShapesModule.java new file mode 100644 index 0000000..38c2f70 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/ShapesModule.java @@ -0,0 +1,378 @@ +package com.sovdee.skriptparticles.skript.shapes; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.Parser; +import ch.njol.skript.expressions.base.EventValueExpression; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.lang.function.Parameter; +import ch.njol.skript.lang.function.SimpleJavaFunction; +import ch.njol.skript.lang.util.ContextlessEvent; +import ch.njol.skript.lang.util.SimpleLiteral; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.registrations.DefaultClasses; +import com.sovdee.shapes.sampling.SamplingStyle; +import com.sovdee.shapes.shapes.CutoffShape; +import com.sovdee.shapes.shapes.LWHShape; +import com.sovdee.shapes.shapes.PolyShape; +import com.sovdee.shapes.shapes.RadialShape; +import com.sovdee.shapes.shapes.Shape; +import com.sovdee.skriptparticles.skript.shapes.expressions.ExprCurrentShape; +import com.sovdee.skriptparticles.skript.shapes.expressions.ExprRotation; +import com.sovdee.skriptparticles.skript.shapes.expressions.ExprShapeCopy; +import com.sovdee.skriptparticles.rendering.Particle; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprArc; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprBezierCurve; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprCircle; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprCuboid; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprEllipse; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprEllipsoid; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprEllipticalArc; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprHeart; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprHelix; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprIrregularPolygon; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprLine; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprRectangle; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprRegularPolygon; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprRegularPolyhedron; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprSphere; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprSphericalCap; +import com.sovdee.skriptparticles.skript.shapes.constructors.ExprStar; +import com.sovdee.skriptparticles.skript.shapes.effects.EffRotateShape; +import com.sovdee.skriptparticles.skript.shapes.effects.EffToggleAxes; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprHelixWindingRate; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeCutoffAngle; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeLWH; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeLocations; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeModifiers; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeNormal; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeOffset; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeOrientation; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeParticle; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeParticleDensity; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapePoints; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeRadius; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeRelativeAxis; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeScale; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeSideLength; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeSides; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprShapeStyle; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprStarPoints; +import com.sovdee.skriptparticles.skript.shapes.properties.ExprStarRadii; +import com.sovdee.skriptparticles.util.Quaternion; +import org.jetbrains.annotations.Nullable; +import org.joml.AxisAngle4f; +import org.joml.Quaternionf; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.HierarchicalAddonModule; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; +import org.skriptlang.skript.lang.converter.Converters; + +public class ShapesModule extends HierarchicalAddonModule { + + public ShapesModule(AddonModule parentModule) { + super(parentModule); + } + + @Override + protected void initSelf(SkriptAddon addon) { + registerShapeTypes(); + registerParticleTypes(); + registerRotationTypes(); + } + + private static void registerShapeTypes() { + Classes.registerClass(new ClassInfo<>(Shape.class, "shape") + .user("shapes?") + .name("Shape") + .description("Represents an abstract particle shape. E.g. circle, line, etc.") + .parser(new Parser<>() { + @Override + public Shape parse(String input, ParseContext context) { return null; } + @Override + public boolean canParse(ParseContext context) { return false; } + @Override + public String toString(Shape o, int flags) { return o.toString(); } + @Override + public String toVariableNameString(Shape shape) { return "shape:" + shape.getPointSampler().getUUID(); } + }) + .cloner(Shape::clone) + ); + + Classes.registerClass(new ClassInfo<>(RadialShape.class, "radialshape") + .user("radial ?shapes?") + .name("Radial Shape") + .description("Represents an abstract particle shape that has a radius. E.g. circle, sphere, etc.") + .parser(new Parser<>() { + @Override + public RadialShape parse(String input, ParseContext context) { return null; } + @Override + public boolean canParse(ParseContext context) { return false; } + @Override + public String toString(RadialShape o, int flags) { return o.toString(); } + @Override + public String toVariableNameString(RadialShape shape) { return "shape:" + shape.getPointSampler().getUUID(); } + }) + ); + + Classes.registerClass(new ClassInfo<>(LWHShape.class, "lwhshape") + .user("lwh ?shapes?") + .name("Length/Width/Height Shape") + .description("Represents an abstract particle shape that has a length, width, and/or height. E.g. cube, cylinder, ellipse, etc.") + .parser(new Parser<>() { + @Override + public LWHShape parse(String input, ParseContext context) { return null; } + @Override + public boolean canParse(ParseContext context) { return false; } + @Override + public String toString(LWHShape o, int flags) { return o.toString(); } + @Override + public String toVariableNameString(LWHShape shape) { return "shape:" + shape.getPointSampler().getUUID(); } + }) + ); + + Classes.registerClass(new ClassInfo<>(CutoffShape.class, "cutoffshape") + .user("cutoff ?shapes?") + .name("Cutoff Shape") + .description("Represents an abstract particle shape that has a cutoff angle. E.g. arc, spherical cap, etc.") + .parser(new Parser<>() { + @Override + public CutoffShape parse(String input, ParseContext context) { return null; } + @Override + public boolean canParse(ParseContext context) { return false; } + @Override + public String toString(CutoffShape o, int flags) { return o.toString(); } + @Override + public String toVariableNameString(CutoffShape shape) { return "shape:" + shape.getPointSampler().getUUID(); } + }) + ); + + Classes.registerClass(new ClassInfo<>(PolyShape.class, "polyshape") + .user("poly ?shapes?") + .name("Polygonal/Polyhedral Shape") + .description( + "Represents an abstract particle shape that is a polygon or polyhedron, with a side length and side count.\n" + + "Irregular shapes are included in this category, but do not support changing either side count or side length." + ) + .parser(new Parser<>() { + @Override + public PolyShape parse(String input, ParseContext context) { return null; } + @Override + public boolean canParse(ParseContext context) { return false; } + @Override + public String toString(PolyShape o, int flags) { return o.toString(); } + @Override + public String toVariableNameString(PolyShape shape) { return "shape:" + shape.getPointSampler().getUUID(); } + }) + ); + + Classes.registerClass(new ClassInfo<>(SamplingStyle.class, "shapestyle") + .user("shape ?styles?") + .name("Shape Style") + .description("Represents the way the shape is drawn. Outlined is a wireframe representation, Surface is filling in all the surfaces of the shape, and Filled is filling in the entire shape.") + .parser(new Parser<>() { + @Override + public @Nullable SamplingStyle parse(String s, ParseContext context) { + s = s.toUpperCase(); + if (s.matches("OUTLINE(D)?") || s.matches("WIREFRAME")) { + return SamplingStyle.OUTLINE; + } else if (s.matches("SURFACE") || s.matches("HOLLOW")) { + return SamplingStyle.SURFACE; + } else if (s.matches("FILL(ED)?") || s.matches("SOLID")) { + return SamplingStyle.FILL; + } + return null; + } + @Override + public boolean canParse(ParseContext context) { return true; } + @Override + public String toString(SamplingStyle style, int i) { return style.toString(); } + @Override + public String toVariableNameString(SamplingStyle style) { return "shapestyle:" + style; } + })); + } + + private static void registerParticleTypes() { + Classes.registerClass(new ClassInfo<>(Particle.class, "customparticle") + .user("customparticles?") + .name("Custom Particle") + .description("Represents a particle with extra shape-related data.") + .parser(new Parser<>() { + @Nullable + @Override + public Particle parse(String s, ParseContext context) { return null; } + @Override + public boolean canParse(ParseContext context) { return false; } + @Override + public String toString(Particle particle, int flags) { + return particle.toString(ContextlessEvent.get(), false); + } + @Override + public String toVariableNameString(Particle particle) { + return "particle:" + toString(particle, 0); + } + }) + ); + + Converters.registerConverter(ParticleEffect.class, Particle.class, Particle::of); + } + + private static void registerRotationTypes() { + if (Classes.getExactClassInfo(Quaternionf.class) == null) { + Classes.registerClass(new ClassInfo<>(Quaternionf.class, "quaternion") + .user("quaternionf?s?") + .name("Quaternion") + .description("Quaternions can be used for shape rotations. They're composed of four values, w, x, y, and z. " + + "See the Quaternion and AxisAngle functions for ways to create them.") + .since("1.0.0") + .parser(new Parser() { + public boolean canParse(ParseContext context) { + return false; + } + @Override + public String toString(Quaternionf quaternion, int flags) { + return "w:" + Skript.toString(quaternion.w()) + ", x:" + Skript.toString(quaternion.x()) + ", y:" + Skript.toString(quaternion.y()) + ", z:" + Skript.toString(quaternion.z()); + } + @Override + public String toVariableNameString(Quaternionf quaternion) { + return quaternion.w() + "," + quaternion.x() + "," + quaternion.y() + "," + quaternion.z(); + } + }) + .defaultExpression(new EventValueExpression<>(Quaternionf.class)) + .cloner(quaternion -> { + try { + return (Quaternionf) quaternion.clone(); + } catch (CloneNotSupportedException e) { + return null; + } + })); + } + + Converters.registerConverter(Quaternionf.class, Quaternion.class, Quaternion::new); + + if (Functions.getGlobalSignature("quaternion") == null) { + Functions.registerFunction(new SimpleJavaFunction<>("quaternion", new Parameter[]{ + new Parameter<>("x", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), + new Parameter<>("y", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), + new Parameter<>("z", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), + new Parameter<>("w", DefaultClasses.NUMBER, true, new SimpleLiteral(1, true)) + }, Classes.getExactClassInfo(Quaternionf.class), true) { + @Override + public @Nullable Quaternionf[] executeSimple(Object[][] params) { + float w = ((Number) params[0][0]).floatValue(); + float x = ((Number) params[1][0]).floatValue(); + float y = ((Number) params[2][0]).floatValue(); + float z = ((Number) params[3][0]).floatValue(); + return new Quaternionf[]{new Quaternionf(x, y, z, w)}; + } + } + .description("Returns a quaternion from the given x, y, z and w parameters.") + .examples("set {_v} to quaternion(0,0,0,1)") + .since("1.0.0")); + } + + if (Functions.getGlobalSignature("axisAngle") == null) { + Functions.registerFunction(new SimpleJavaFunction<>("axisAngle", new Parameter[]{ + new Parameter<>("angle", DefaultClasses.NUMBER, true, null), + new Parameter<>("x", DefaultClasses.NUMBER, true, null), + new Parameter<>("y", DefaultClasses.NUMBER, true, null), + new Parameter<>("z", DefaultClasses.NUMBER, true, null) + }, Classes.getExactClassInfo(Quaternionf.class), true) { + @Override + public @Nullable Quaternionf[] executeSimple(Object[][] params) { + float angle = ((Number) params[0][0]).floatValue(); + float x = ((Number) params[1][0]).floatValue(); + float y = ((Number) params[2][0]).floatValue(); + float z = ((Number) params[3][0]).floatValue(); + AxisAngle4f axisAngle4f = new AxisAngle4f(angle, x, y, z); + return new Quaternionf[]{new Quaternionf(axisAngle4f)}; + } + } + .description("Returns a quaternion from the given axis and angle parameters. The axis is a vector composed of 3 numbers, x, y, and z, and the angle is the rotation around that axis, in radians.") + .examples("set {_v} to axisAngle(3.14, 1, 0, 0)") + .since("1.0.0")); + } + + if (Functions.getGlobalSignature("axisAngleDegrees") == null) { + Functions.registerFunction(new SimpleJavaFunction<>("axisAngleDegrees", new Parameter[]{ + new Parameter<>("angle", DefaultClasses.NUMBER, true, null), + new Parameter<>("x", DefaultClasses.NUMBER, true, null), + new Parameter<>("y", DefaultClasses.NUMBER, true, null), + new Parameter<>("z", DefaultClasses.NUMBER, true, null) + }, Classes.getExactClassInfo(Quaternionf.class), true) { + @Override + public @Nullable Quaternionf[] executeSimple(Object[][] params) { + float angle = ((Number) params[0][0]).floatValue() * (float) Math.PI / 180; + float x = ((Number) params[1][0]).floatValue(); + float y = ((Number) params[2][0]).floatValue(); + float z = ((Number) params[3][0]).floatValue(); + AxisAngle4f axisAngle4f = new AxisAngle4f(angle, x, y, z); + return new Quaternionf[]{new Quaternionf(axisAngle4f)}; + } + } + .description("Returns a quaternion from the given axis and angle parameters. The axis is a vector composed of 3 numbers, x, y, and z, and the angle is the rotation around that axis, in degrees.") + .examples("set {_v} to axisAngleDegrees(180, 1, 0, 0)") + .since("1.0.0")); + } + } + + @Override + protected void loadSelf(SkriptAddon addon) { + register(addon, + // shape + ExprCurrentShape::register, + // shape factory expressions + ExprCircle::register, + ExprSphere::register, + ExprCuboid::register, + ExprLine::register, + ExprArc::register, + ExprEllipse::register, + ExprEllipsoid::register, + ExprRegularPolygon::register, + ExprRegularPolyhedron::register, + ExprHelix::register, + ExprBezierCurve::register, + ExprIrregularPolygon::register, + ExprRectangle::register, + ExprHeart::register, + ExprSphericalCap::register, + ExprStar::register, + ExprEllipticalArc::register, + // misc shape expressions + ExprShapeCopy::register, + ExprRotation::register, + // shape property expressions + ExprShapeRadius::register, + ExprShapeParticle::register, + ExprShapeStyle::register, + ExprShapeScale::register, + ExprShapeOffset::register, + ExprShapeOrientation::register, + ExprShapeNormal::register, + ExprShapeParticleDensity::register, + ExprShapeRelativeAxis::register, + ExprShapeLWH::register, + ExprShapeCutoffAngle::register, + ExprShapeSides::register, + ExprShapeSideLength::register, + ExprShapePoints::register, + ExprShapeLocations::register, + ExprShapeModifiers::register, + ExprHelixWindingRate::register, + ExprStarPoints::register, + ExprStarRadii::register, + // shape effects + EffRotateShape::register, + EffToggleAxes::register + ); + } + + @Override + public String name() { + return "shapes"; + } + +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprArc.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprArc.java similarity index 62% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprArc.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprArc.java index b137d0c..ee82e21 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprArc.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprArc.java @@ -1,43 +1,48 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Arc; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Arc or Sector") -@Description({ - "Creates an arc or sector with the given radius and cutoff angle. The radius must be greater than 0 and the height, if given, must be positive.", - "The angle must be between 0 and 360 degrees. If the angle is 360 degrees, the shape will be a circle or cylinder.", - "An arc is a portion of the circle's circumference. A sector is a portion of the circle's area." -}) -@Examples({ - "set {_shape} to an arc with radius 10 and angle 45 degrees", - "set {_shape} to a circular sector of radius 3 and angle 90 degrees", - "set {_shape} to a sector of radius 3 and height 5 and angle 90 degrees", - "set {_shape} to a cylindrical sector of radius 1, height 0.5, and angle 45" -}) +@Description(""" + Creates an arc or sector with the given radius and cutoff angle. The radius must be greater than 0 and the height, if given, must be positive. + The angle must be between 0 and 360 degrees. If the angle is 360 degrees, the shape will be a circle or cylinder. + An arc is a portion of the circle's circumference. A sector is a portion of the circle's area. + """) +@Example("set {_shape} to an arc with radius 10 and angle 45 degrees") +@Example("set {_shape} to a circular sector of radius 3 and angle 90 degrees") +@Example("set {_shape} to a sector of radius 3 and height 5 and angle 90 degrees") +@Example("set {_shape} to a cylindrical sector of radius 1, height 0.5, and angle 45") @Since("1.0.0") -public class ExprArc extends SimpleExpression { - - static { - Skript.registerExpression(ExprArc.class, Shape.class, ExpressionType.COMBINED, - "[a[n]] [circular] (arc|:sector) (with|of) radius %number% and [cutoff] angle [of] %number% [degrees|:radians]", - "[a[n]] [cylindrical] (arc|:sector) (with|of) radius %number%(,| and) height %-number%[,] and [cutoff] angle [of] %number% [degrees|:radians]"); +public class ExprArc extends ShapeConstructorExpression { + + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprArc.class, Shape.class) + .supplier(ExprArc::new) + .addPatterns( + "[a[n]] [circular] (arc|:sector) (with|of) radius %number% and [cutoff] angle [of] %number% [degrees|:radians]", + "[a[n]] [cylindrical] (arc|:sector) (with|of) radius %number%(,| and) height %-number%[,] and [cutoff] angle [of] %number% [degrees|:radians]" + ) + .build()); } private Expression radius; @@ -47,7 +52,8 @@ public class ExprArc extends SimpleExpression { private boolean isSector = false; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { radius = (Expression) exprs[0]; if (matchedPattern == 1) { height = (Expression) exprs[1]; @@ -80,8 +86,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Number radius = this.radius.getSingle(event); Number angle = this.angle.getSingle(event); Number height = (this.height != null) ? this.height.getSingle(event) : 0; @@ -100,17 +105,7 @@ protected Shape[] get(Event event) { shape.getPointSampler().setStyle(SamplingStyle.FILL); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprBezierCurve.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprBezierCurve.java similarity index 55% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprBezierCurve.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprBezierCurve.java index 5816811..d4ab074 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprBezierCurve.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprBezierCurve.java @@ -1,43 +1,42 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; import com.sovdee.shapes.shapes.BezierCurve; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.Point; import com.sovdee.skriptparticles.util.VectorConversion; import org.bukkit.Location; import org.bukkit.event.Event; -import org.checkerframework.checker.nullness.qual.NonNull; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Vector3d; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import java.util.ArrayList; import java.util.List; @Name("Particle Bezier Curve") -@Description({ - "Creates a bezier curve between the given start and end points, using the given control points to change the curve." -}) -@Examples({ - "set {_shape} to a bezier curve from {a} to {b} with control points {c} and {d}", - "set {_shape} to a curve from player to player's target with control point (location 3 above player)" -}) +@Description(""" + Creates a bezier curve between the given start and end points, using the given control points to change the curve. + """) +@Example("set {_shape} to a bezier curve from {a} to {b} with control points {c} and {d}") +@Example("set {_shape} to a curve from player to player's target with control point (location 3 above player)") @Since("1.3.0") -public class ExprBezierCurve extends SimpleExpression { +public class ExprBezierCurve extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprBezierCurve.class, Shape.class, ExpressionType.COMBINED, - "[a] [bezier] curve from [start] %vector/entity/location% to [end] %vector/entity/location% (with|using) control point[s] %vectors/entities/locations%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprBezierCurve.class, Shape.class) + .supplier(ExprBezierCurve::new) + .addPatterns("[a] [bezier] curve from [start] %vector/entity/location% to [end] %vector/entity/location% (with|using) control point[s] %vectors/entities/locations%") + .build()); } private Expression start; @@ -45,7 +44,7 @@ public class ExprBezierCurve extends SimpleExpression { private Expression controlPoints; @Override - public boolean init(Expression[] exprs, int matchedPattern, @NonNull Kleenean isDelayed, @NonNull ParseResult parseResult) { + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { start = exprs[0]; end = exprs[1]; controlPoints = exprs[2]; @@ -53,7 +52,7 @@ public boolean init(Expression[] exprs, int matchedPattern, @NonNull Kleenean } @Override - protected Shape @Nullable [] get(@NonNull Event event) { + protected @Nullable List getShapes(Event event) { @Nullable Point startPt = Point.of(this.start.getSingle(event)); @Nullable Point endPt = Point.of(this.end.getSingle(event)); if (startPt == null || endPt == null) @@ -62,13 +61,10 @@ public boolean init(Expression[] exprs, int matchedPattern, @NonNull Kleenean for (Object value : this.controlPoints.getArray(event)) { controlPts.add(Point.of(value)); } - - // Use Supplier-based BezierCurve for dynamic control points - BezierCurve curve = getBezierCurve(startPt, controlPts, endPt); - return new Shape[]{curve}; + return List.of(getBezierCurve(startPt, controlPts, endPt)); } - private static @NonNull BezierCurve getBezierCurve(@NonNull Point startPt, List> controlPts, @NonNull Point endPt) { + private static @NotNull BezierCurve getBezierCurve(@NotNull Point startPt, List> controlPts, @NotNull Point endPt) { BezierCurve curve = new BezierCurve(() -> { Location origin = startPt.getLocation(); List result = new ArrayList<>(); @@ -83,18 +79,6 @@ public boolean init(Expression[] exprs, int matchedPattern, @NonNull Kleenean } @Override - public boolean isSingle() { - return true; - } - - @Override - @NonNull - public Class getReturnType() { - return Shape.class; - } - - @Override - @NonNull public String toString(@Nullable Event event, boolean debug) { return "bezier curve between start " + start.toString(event, debug) + " and end " + end.toString(event, debug) + " using control points " + controlPoints.toString(event, debug); diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprCircle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprCircle.java similarity index 66% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprCircle.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprCircle.java index a71f273..f8cb4cb 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprCircle.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprCircle.java @@ -1,41 +1,44 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Circle; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; -import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Circle or Cylinder") -@Description({ - "Creates a circle, disc, or cylinder shape with the given radius. The radius must be greater than 0 and the height cannot be negative." -}) -@Examples({ - "set {_shape} to circle with radius 10", - "set {_shape} to a disc of radius 3", - "set {_shape} to a solid cylinder with radius 3 and height 5" -}) +@Description(""" + Creates a circle, disc, or cylinder shape with the given radius. The radius must be greater than 0 and the height cannot be negative. + """) +@Example("set {_shape} to circle with radius 10") +@Example("set {_shape} to a disc of radius 3") +@Example("set {_shape} to a solid cylinder with radius 3 and height 5") @Since("1.0.0") -public class ExprCircle extends SimpleExpression { +public class ExprCircle extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprCircle.class, Shape.class, ExpressionType.COMBINED, - "[a] (circle|:disc) (with|of) radius %number%", - "[a] [hollow|2:solid] (cylinder|1:tube) (with|of) radius %number% and height %number%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprCircle.class, Shape.class) + .supplier(ExprCircle::new) + .addPatterns( + "[a] (circle|:disc) (with|of) radius %number%", + "[a] [hollow|2:solid] (cylinder|1:tube) (with|of) radius %number% and height %number%" + ) + .build()); } private Expression radius; @@ -44,7 +47,8 @@ public class ExprCircle extends SimpleExpression { private boolean isCylinder; @Override - public boolean init(Expression[] exprs, int matchedPattern, @NonNull Kleenean isDelayed, @NonNull ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { isCylinder = matchedPattern == 1; radius = (Expression) exprs[0]; @@ -74,8 +78,7 @@ public boolean init(Expression[] exprs, int matchedPattern, @NonNull Kleenean } @Override - @Nullable - protected Shape[] get(@NonNull Event event) { + protected @Nullable List getShapes(Event event) { Number radius = this.radius.getSingle(event); Number height = this.height != null ? this.height.getSingle(event) : 0; if (radius == null || height == null) @@ -87,22 +90,10 @@ protected Shape[] get(@NonNull Event event) { Circle shape = new Circle(radius.doubleValue(), height.doubleValue()); shape.getPointSampler().setStyle(style); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - @NonNull - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override - @NonNull public String toString(@Nullable Event event, boolean debug) { return (isCylinder ? "circle of radius " + radius.toString(event, debug) : "cylinder of radius " + radius.toString(event, debug) + " and height " + (height != null ? height.toString(event, debug) : "0")); diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprCuboid.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprCuboid.java similarity index 66% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprCuboid.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprCuboid.java index e0f0d21..7a35d8a 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprCuboid.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprCuboid.java @@ -1,47 +1,49 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Cuboid; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.DynamicLocation; import com.sovdee.skriptparticles.util.MathUtil; import com.sovdee.skriptparticles.util.VectorConversion; import org.bukkit.event.Event; import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Cuboid") -@Description({ - "Creates a cuboid from a length, a width, and a height, or from two corners.", - "The specified length, width, and height must be greater than 0. Length is the x-axis, width is the z-axis, and height is the y-axis.", - "When defining a cuboid from two corners, the corners can either be vectors or locations/entities. " + - "You cannot use both vectors and locations/entities, but you can mix and match locations and entities." + - "When using locations, this is a shape that can be drawn without a specific location. It will be drawn between the two given locations.", -}) -@Examples({ - "set {_shape} to a solid cuboid with length 10, width 10, and height 10", - "set {_shape} to a hollow cuboid from vector(-5, -5, -5) to vector(5, 5, 5)", - "draw the shape of a cuboid from player to player's target" -}) +@Description(""" + Creates a cuboid from a length, a width, and a height, or from two corners. + The specified length, width, and height must be greater than 0. Length is the x-axis, width is the z-axis, and height is the y-axis. + When defining a cuboid from two corners, the corners can either be vectors or locations/entities. + You cannot use both vectors and locations/entities, but you can mix and match locations and entities. + When using locations, this is a shape that can be drawn without a specific location. It will be drawn between the two given locations. + """) +@Example("set {_shape} to a solid cuboid with length 10, width 10, and height 10") +@Example("set {_shape} to a hollow cuboid from vector(-5, -5, -5) to vector(5, 5, 5)") +@Example("draw the shape of a cuboid from player to player's target") @Since("1.0.0") -public class ExprCuboid extends SimpleExpression { - - static { - Skript.registerExpression(ExprCuboid.class, Shape.class, ExpressionType.COMBINED, - "[a] [:hollow|:solid] cuboid (with|of) length %number%(,| and) width %number%[,] and height %number%", - "[a] [:hollow|:solid] cuboid (from|between) %location/entity/vector% (to|and) %location/entity/vector%"); +public class ExprCuboid extends ShapeConstructorExpression { + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprCuboid.class, Shape.class) + .supplier(ExprCuboid::new) + .addPatterns( + "[a] [:hollow|:solid] cuboid (with|of) length %number%(,| and) width %number%[,] and height %number%", + "[a] [:hollow|:solid] cuboid (from|between) %location/entity/vector% (to|and) %location/entity/vector%" + ) + .build()); } private Expression width; @@ -50,11 +52,11 @@ public class ExprCuboid extends SimpleExpression { private Expression corner1; private Expression corner2; private int matchedPattern = 0; - private SamplingStyle style; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { switch (matchedPattern) { case 0 -> { length = (Expression) exprs[0]; @@ -78,10 +80,8 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Shape shape; - // from width, length, height if (matchedPattern == 0) { if (width == null || length == null || height == null) return null; Number width = this.width.getSingle(event); @@ -92,14 +92,12 @@ protected Shape[] get(Event event) { length = Math.max(length.doubleValue(), MathUtil.EPSILON); height = Math.max(height.doubleValue(), MathUtil.EPSILON); shape = new Cuboid(length.doubleValue(), width.doubleValue(), height.doubleValue()); - // from location/entity/vector to location/entity/vector } else { if (corner1 == null || corner2 == null) return null; Object corner1 = this.corner1.getSingle(event); Object corner2 = this.corner2.getSingle(event); if (corner1 == null || corner2 == null) return null; - // vector check if (corner1 instanceof Vector && corner2 instanceof Vector) { shape = new Cuboid(VectorConversion.toJOML((Vector) corner1), VectorConversion.toJOML((Vector) corner2)); } else if (corner1 instanceof Vector || corner2 instanceof Vector) { @@ -109,7 +107,6 @@ protected Shape[] get(Event event) { DynamicLocation dl2 = DynamicLocation.fromLocationEntity(corner2); if (dl1 == null || dl2 == null) return null; - // Use Supplier-based Cuboid for dynamic corners shape = new Cuboid( () -> VectorConversion.toJOML(dl1.getLocation().toVector()), () -> VectorConversion.toJOML(dl2.getLocation().toVector()) @@ -118,17 +115,7 @@ protected Shape[] get(Event event) { } shape.getPointSampler().setStyle(style); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override @@ -144,6 +131,5 @@ public String toString(@Nullable Event event, boolean debug) { case 1 -> "from " + corner1.toString(event, debug) + " to " + corner2.toString(event, debug); default -> ""; }; - } } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipse.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipse.java similarity index 64% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipse.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipse.java index 40f2a55..4813ac4 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipse.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipse.java @@ -1,43 +1,47 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Ellipse; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Ellipse or Elliptical Cylinder") -@Description({ - "Creates a ellipse, elliptical disc, or elliptical cylinder shape with the given radii. The radii must be greater than 0.", - "The first radius is the x radius, and the second radius is the z radius. These are relative to the shape's rotation, " + - "so they only correspond exactly to the x and z axes if the shape is not rotated.", - "NOTE: Very eccentric elliptical discs/sectors (those with a large difference between the x and z radii) may have many more particles than expected. Be careful." -}) -@Examples({ - "set {_shape} to oval with radii 10 and 3", - "set {_shape} to a solid ellipse of radius 3 and 5", - "set {_shape} to a hollow elliptical cylinder with radii 3 and 6 and height 5" -}) +@Description(""" + Creates an ellipse, elliptical disc, or elliptical cylinder shape with the given radii. The radii must be greater than 0. + The first radius is the x radius, and the second radius is the z radius. These are relative to the shape's rotation, + so they only correspond exactly to the x and z axes if the shape is not rotated. + NOTE: Very eccentric elliptical discs/sectors (those with a large difference between the x and z radii) may have many more particles than expected. Be careful. + """) +@Example("set {_shape} to oval with radii 10 and 3") +@Example("set {_shape} to a solid ellipse of radius 3 and 5") +@Example("set {_shape} to a hollow elliptical cylinder with radii 3 and 6 and height 5") @Since("1.0.0") -public class ExprEllipse extends SimpleExpression { +public class ExprEllipse extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprEllipse.class, Shape.class, ExpressionType.COMBINED, - "[a[n]] [surface:(solid|filled)] (ellipse|oval) (with|of) radi(i|us) %number% and %number%", - "[a[n]] [hollow|2:solid] elliptical (cylinder|1:tube) (with|of) radi(i|us) %number%(,| and) %number%[,] and height %number%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprEllipse.class, Shape.class) + .supplier(ExprEllipse::new) + .addPatterns( + "[a[n]] [surface:(solid|filled)] (ellipse|oval) (with|of) radi(i|us) %number% and %number%", + "[a[n]] [hollow|2:solid] elliptical (cylinder|1:tube) (with|of) radi(i|us) %number%(,| and) %number%[,] and height %number%" + ) + .build()); } private Expression xRadius; @@ -46,7 +50,8 @@ public class ExprEllipse extends SimpleExpression { private SamplingStyle style; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { xRadius = (Expression) exprs[0]; zRadius = (Expression) exprs[1]; style = SamplingStyle.OUTLINE; @@ -84,8 +89,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Number xRadius = this.xRadius.getSingle(event); Number zRadius = this.zRadius.getSingle(event); Number height = this.height == null ? 0 : this.height.getSingle(event); @@ -99,17 +103,7 @@ protected Shape[] get(Event event) { Ellipse shape = new Ellipse(xRadius.doubleValue(), zRadius.doubleValue(), height.doubleValue()); shape.getPointSampler().setStyle(style); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipsoid.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipsoid.java similarity index 59% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipsoid.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipsoid.java index 7a5c822..fd19cd7 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipsoid.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipsoid.java @@ -1,44 +1,46 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Ellipsoid; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Ellipsoid") -@Description({ - "Creates a ellipsoid shape with the given radii. The radii must be greater than 0.", - "The first radius is the x radius, and the second is the y radius, and the last is the z radius. " + - "These are relative to the shape's rotation, so they only correspond exactly to the world axes if the shape is not rotated.", - "Note that this shape is modified using the Length/Width/Height modifiers, not the Radius modifier. This means the length/width/height " + - "of the shape will be twice the radius in each direction. Length is the x axis, width is the z axis, and height is the y axis.", - "NOTE: Very eccentric solid ellipsoids (those with a large difference between the radii) may have many more particles than expected. Be careful." -}) -@Examples({ - "set {_shape} to ellipsoid with radii 10, 3, and 8", - "set {_shape} to a solid ellipsoid of radius 3 and 5 and 6", - "set {_shape} to a hollow ellipsoid with radii 3, 6 and 5" -}) +@Description(""" + Creates an ellipsoid shape with the given radii. The radii must be greater than 0. + The first radius is the x radius, the second is the y radius, and the last is the z radius. + These are relative to the shape's rotation, so they only correspond exactly to the world axes if the shape is not rotated. + Note that this shape is modified using the Length/Width/Height modifiers, not the Radius modifier. This means the length/width/height + of the shape will be twice the radius in each direction. Length is the x axis, width is the z axis, and height is the y axis. + NOTE: Very eccentric solid ellipsoids (those with a large difference between the radii) may have many more particles than expected. Be careful. + """) +@Example("set {_shape} to ellipsoid with radii 10, 3, and 8") +@Example("set {_shape} to a solid ellipsoid of radius 3 and 5 and 6") +@Example("set {_shape} to a hollow ellipsoid with radii 3, 6 and 5") @Since("1.0.0") -public class ExprEllipsoid extends SimpleExpression { +public class ExprEllipsoid extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprEllipsoid.class, Shape.class, ExpressionType.COMBINED, - "[a[n]] [(outlined|wireframe|:hollow|fill:(solid|filled))] ellipsoid (with|of) radi(i|us) %number%(,| and) %number%[,] and %number%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprEllipsoid.class, Shape.class) + .supplier(ExprEllipsoid::new) + .addPatterns("[a[n]] [(outlined|wireframe|:hollow|fill:(solid|filled))] ellipsoid (with|of) radi(i|us) %number%(,| and) %number%[,] and %number%") + .build()); } private Expression xRadius; @@ -47,7 +49,8 @@ public class ExprEllipsoid extends SimpleExpression { private SamplingStyle style; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { xRadius = (Expression) exprs[0]; yRadius = (Expression) exprs[1]; zRadius = (Expression) exprs[2]; @@ -75,8 +78,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Number xRadius = this.xRadius.getSingle(event); Number yRadius = this.yRadius.getSingle(event); Number zRadius = this.zRadius.getSingle(event); @@ -90,22 +92,11 @@ protected Shape[] get(Event event) { Ellipsoid shape = new Ellipsoid(xRadius.doubleValue(), yRadius.doubleValue(), zRadius.doubleValue()); shape.getPointSampler().setStyle(style); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override public String toString(@Nullable Event event, boolean debug) { return "ellipsoid with radii " + xRadius.toString(event, debug) + ", " + yRadius.toString(event, debug) + ", and " + zRadius.toString(event, debug); } - } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipticalArc.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipticalArc.java similarity index 63% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipticalArc.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipticalArc.java index 29ef979..107f1bb 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprEllipticalArc.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprEllipticalArc.java @@ -1,43 +1,47 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.EllipticalArc; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Elliptical Arc or Sector") -@Description({ - "Creates an elliptical arc or sector with the given radii and cutoff angle. The radii must be greater than 0 and the height, if given, must be positive.", - "The angle must be between 0 and 360 degrees. If the angle is 360 degrees, the shape will be a ellipse or elliptical cylinder.", - "An arc is a portion of the ellipse's circumference. A sector is a portion of the ellipse's area.", - "NOTE: Very eccentric elliptical sectors (those with a large difference between the x and z radii) may have many more particles than expected. Be careful." -}) -@Examples({ - "set {_shape} to an elliptical arc with radii 10 and 3 and cutoff angle of 90 degrees", - "set {_shape} to a elliptical sector of radius 3 and 5 and cutoff angle of 45 degrees", - "set {_shape} to a elliptical cylinder with radii 3 and 5, height 10, and cutoff angle of 3.1415 radians" -}) +@Description(""" + Creates an elliptical arc or sector with the given radii and cutoff angle. The radii must be greater than 0 and the height, if given, must be positive. + The angle must be between 0 and 360 degrees. If the angle is 360 degrees, the shape will be an ellipse or elliptical cylinder. + An arc is a portion of the ellipse's circumference. A sector is a portion of the ellipse's area. + NOTE: Very eccentric elliptical sectors (those with a large difference between the x and z radii) may have many more particles than expected. Be careful. + """) +@Example("set {_shape} to an elliptical arc with radii 10 and 3 and cutoff angle of 90 degrees") +@Example("set {_shape} to a elliptical sector of radius 3 and 5 and cutoff angle of 45 degrees") +@Example("set {_shape} to a elliptical cylinder with radii 3 and 5, height 10, and cutoff angle of 3.1415 radians") @Since("1.0.0") -public class ExprEllipticalArc extends SimpleExpression { - - static { - Skript.registerExpression(ExprEllipticalArc.class, Shape.class, ExpressionType.COMBINED, - "[an] elliptical (arc|:sector) (with|of) radi(i|us) %number%(,| and) %number%[,] and [cutoff] angle [of] %number% [degrees|:radians]", - "[an] elliptical [cylindrical] (arc|:sector) (with|of) radi(i|us) %number%(,| and) %number%(,| and) height %-number%[,] and [cutoff] angle [of] %number% [degrees|:radians]"); +public class ExprEllipticalArc extends ShapeConstructorExpression { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprEllipticalArc.class, Shape.class) + .supplier(ExprEllipticalArc::new) + .addPatterns( + "[an] elliptical (arc|:sector) (with|of) radi(i|us) %number%(,| and) %number%[,] and [cutoff] angle [of] %number% [degrees|:radians]", + "[an] elliptical [cylindrical] (arc|:sector) (with|of) radi(i|us) %number%(,| and) %number%(,| and) height %-number%[,] and [cutoff] angle [of] %number% [degrees|:radians]" + ) + .build()); } private Expression xRadius; @@ -48,10 +52,8 @@ public class ExprEllipticalArc extends SimpleExpression { private boolean isRadians; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { -// Skript.error("Elliptical arcs are currently disabled. If you know how to efficiently compute the inverse of the " + -// "elliptic integral of the second kind, please send me a message on Discord or Github."); -// return false; + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { xRadius = (Expression) exprs[0]; zRadius = (Expression) exprs[1]; if (matchedPattern == 1) { @@ -92,8 +94,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Number xRadius = this.xRadius.getSingle(event); Number zRadius = this.zRadius.getSingle(event); Number height = this.height == null ? 0 : this.height.getSingle(event); @@ -111,17 +112,7 @@ protected Shape[] get(Event event) { EllipticalArc shape = new EllipticalArc(xRadius.doubleValue(), zRadius.doubleValue(), height.doubleValue(), angle.doubleValue()); shape.getPointSampler().setStyle(style); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprHeart.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprHeart.java similarity index 60% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprHeart.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprHeart.java index c807907..8371cb4 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprHeart.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprHeart.java @@ -1,41 +1,45 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Heart; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Heart") -@Description({ - "Creates a heart shape with the given width and height, and optionally eccentricity. The width (x) and length (z) must be greater than 0.", - "The eccentricity defaults to 3, but must be at least 1. This determines how round/pointy the heart is. Values between 1 and 5 are recommended.", - "Note that the width and length are not exact, but they're roughly the width and length of the heart.", - "Finally, this shape does not support the particle count expression and its particle density is not uniform. If anyone knows a good way to compute the complete elliptic integral of the second kind, please let me know." -}) -@Examples({ - "set {_heart} to heart with width 5 and length 4", - "set {_heart} to heart shape with width 5, length 7, and eccentricity 2", - "draw the shape of a heart of width 2 and length 2 at player" -}) +@Description(""" + Creates a heart shape with the given width and height, and optionally eccentricity. The width (x) and length (z) must be greater than 0. + The eccentricity defaults to 3, but must be at least 1. This determines how round/pointy the heart is. Values between 1 and 5 are recommended. + Note that the width and length are not exact, but they're roughly the width and length of the heart. + Finally, this shape does not support the particle count expression and its particle density is not uniform. + If anyone knows a good way to compute the complete elliptic integral of the second kind, please let me know. + """) +@Example("set {_heart} to heart with width 5 and length 4") +@Example("set {_heart} to heart shape with width 5, length 7, and eccentricity 2") +@Example("draw the shape of a heart of width 2 and length 2 at player") @Since("1.0.1") -public class ExprHeart extends SimpleExpression { +public class ExprHeart extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprHeart.class, Shape.class, ExpressionType.COMBINED, "[a] [:solid] heart [shape] (with|of) width %number%[,] [and] length %number%[[,] [and] eccentricity %-number%]"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprHeart.class, Shape.class) + .supplier(ExprHeart::new) + .addPatterns("[a] [:solid] heart [shape] (with|of) width %number%[,] [and] length %number%[[,] [and] eccentricity %-number%]") + .build()); } private Expression width; @@ -44,7 +48,8 @@ public class ExprHeart extends SimpleExpression { private boolean isSolid; @Override - public boolean init(Expression[] expressions, int i, Kleenean kleenean, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] expressions, int matchedPattern, Kleenean kleenean, ParseResult parseResult) { width = (Expression) expressions[0]; length = (Expression) expressions[1]; if (expressions.length > 2) { @@ -67,19 +72,16 @@ public boolean init(Expression[] expressions, int i, Kleenean kleenean, Parse } isSolid = parseResult.hasTag("solid"); - return true; } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Number width = this.width.getSingle(event); Number length = this.length.getSingle(event); Number eccentricity = this.eccentricity == null ? 3 : this.eccentricity.getSingle(event); - if (width == null || length == null || eccentricity == null) { + if (width == null || length == null || eccentricity == null) return null; - } width = Math.max(width.doubleValue(), MathUtil.EPSILON); length = Math.max(length.doubleValue(), MathUtil.EPSILON); eccentricity = Math.max(eccentricity.doubleValue(), 1); @@ -89,17 +91,7 @@ protected Shape[] get(Event event) { shape.getPointSampler().setStyle(SamplingStyle.SURFACE); } shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprHelix.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprHelix.java similarity index 64% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprHelix.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprHelix.java index 83c8cae..d04dfd6 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprHelix.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprHelix.java @@ -1,41 +1,43 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Helix; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Helix / Spiral") -@Description({ - "Creates a helix or spiral shape with the given radius and height. The radius and height must be greater than 0.", - "The winding rate is the number of loops per meter or block. If omitted, the winding rate will be 1 loop per block.", - "The height of the helix can be manipulated through both the length and height expressions." -}) -@Examples({ - "set {_shape} to helix with radius 10 and height 5", - "set {_shape} to a spiral with radius 3 and height 5 and winding rate of 2 loops per meter", - "set {_shape} to a solid anti-clockwise helix with radius 3, height 5, winding rate 1" -}) +@Description(""" + Creates a helix or spiral shape with the given radius and height. The radius and height must be greater than 0. + The winding rate is the number of loops per meter or block. If omitted, the winding rate will be 1 loop per block. + The height of the helix can be manipulated through both the length and height expressions. + """) +@Example("set {_shape} to helix with radius 10 and height 5") +@Example("set {_shape} to a spiral with radius 3 and height 5 and winding rate of 2 loops per meter") +@Example("set {_shape} to a solid anti-clockwise helix with radius 3, height 5, winding rate 1") @Since("1.0.0") -public class ExprHelix extends SimpleExpression { +public class ExprHelix extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprHelix.class, Shape.class, ExpressionType.COMBINED, - "[a[n]] [:solid] [[1:(counter|anti)[-]]clockwise] (helix|spiral) (with|of) radius %number%[,] [and] height %number%[[,] [and] winding rate [of] %-number% [loops per (meter|block)]]"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprHelix.class, Shape.class) + .supplier(ExprHelix::new) + .addPatterns("[a[n]] [:solid] [[1:(counter|anti)[-]]clockwise] (helix|spiral) (with|of) radius %number%[,] [and] height %number%[[,] [and] winding rate [of] %-number% [loops per (meter|block)]]") + .build()); } private Expression radius; @@ -46,7 +48,8 @@ public class ExprHelix extends SimpleExpression { private SamplingStyle style; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { radius = (Expression) exprs[0]; height = (Expression) exprs[1]; if (exprs.length > 2) @@ -76,13 +79,12 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { @Nullable Number radius = this.radius.getSingle(event); @Nullable Number height = this.height.getSingle(event); @Nullable Number windingRate = this.windingRate == null ? 1 : this.windingRate.getSingle(event); if (radius == null || height == null || windingRate == null) - return new Shape[0]; + return null; radius = Math.max(radius.doubleValue(), MathUtil.EPSILON); height = Math.max(height.doubleValue(), MathUtil.EPSILON); @@ -91,17 +93,7 @@ protected Shape[] get(Event event) { Helix shape = new Helix(radius.doubleValue(), height.doubleValue(), slope / (2 * Math.PI), direction); shape.getPointSampler().setStyle(style); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override @@ -110,5 +102,4 @@ public String toString(@Nullable Event event, boolean debug) { radius.toString(event, debug) + ", height " + height.toString(event, debug) + (windingRate == null ? "" : ", and winding rate " + windingRate.toString(event, debug)); } - } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprIrregularPolygon.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprIrregularPolygon.java similarity index 62% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprIrregularPolygon.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprIrregularPolygon.java index 864fdc0..964f497 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprIrregularPolygon.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprIrregularPolygon.java @@ -1,19 +1,17 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; import com.sovdee.shapes.shapes.IrregularPolygon; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.DynamicLocation; import com.sovdee.skriptparticles.util.VectorConversion; import org.bukkit.Location; @@ -21,34 +19,37 @@ import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; import org.joml.Vector3d; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import java.util.ArrayList; import java.util.List; @Name("Particle Irregular Polygon") -@Description({ - "Creates an irregular polygon from a list of vectors or locations. If locations are used, the polygon can be drawn without giving a specific location to draw at.", - "The height of the polygon will be the height between the lowest and highest points. It can also be set with the optional height parameter.", - "", - "Irregular polygons currently only support the wireframe style. Also, they do not currently support Dynamic Locations like lines and cuboids do." -}) -@Examples({ - "set {_shape} to a polygon with points vector(0, 0, 0), vector(1, 0, 0), and vector(1, 1, 1)", - "set {_shape} to a 2d polygon from points vector(0,0,1), vector(1,0,1), vector(0,0,-1) and height 0.5" -}) +@Description(""" + Creates an irregular polygon from a list of vectors or locations. If locations are used, the polygon can be drawn without giving a specific location to draw at. + The height of the polygon will be the height between the lowest and highest points. It can also be set with the optional height parameter. + + Irregular polygons currently only support the wireframe style. Also, they do not currently support Dynamic Locations like lines and cuboids do. + """) +@Example("set {_shape} to a polygon with points vector(0, 0, 0), vector(1, 0, 0), and vector(1, 1, 1)") +@Example("set {_shape} to a 2d polygon from points vector(0,0,1), vector(1,0,1), vector(0,0,-1) and height 0.5") @Since("1.0.0") -public class ExprIrregularPolygon extends SimpleExpression { +public class ExprIrregularPolygon extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprIrregularPolygon.class, Shape.class, ExpressionType.COMBINED, - "[a] [2d] polygon (from|with) [vertices|points] %vectors/locations% [and height %-number%]"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprIrregularPolygon.class, Shape.class) + .supplier(ExprIrregularPolygon::new) + .addPatterns("[a] [2d] polygon (from|with) [vertices|points] %vectors/locations% [and height %-number%]") + .build()); } private Expression points; private Expression height; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { points = exprs[0]; if (exprs.length > 1) { height = (Expression) exprs[1]; @@ -59,13 +60,11 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye return false; } } - return true; } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Object[] points = this.points.getArray(event); List vertices = new ArrayList<>(points.length); Vector locationOffset = null; @@ -91,17 +90,7 @@ protected Shape[] get(Event event) { if (locationOffset != null) { DrawData.of(shape).setLocation(new DynamicLocation((Location) points[0])); } - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprLine.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprLine.java similarity index 70% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprLine.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprLine.java index 0b9ba2d..cf740b9 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprLine.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprLine.java @@ -1,71 +1,71 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; import com.sovdee.shapes.shapes.Line; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.DynamicLocation; import com.sovdee.skriptparticles.util.MathUtil; import com.sovdee.skriptparticles.util.VectorConversion; import org.bukkit.event.Event; import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import java.util.ArrayList; import java.util.List; @Name("Particle Line") -@Description({ - "Creates a line shape between points, or in a direction for a given length. The length must be greater than 0.", - "When defining a line from points, the points can either be vectors or locations/entities. Each point in the first set will connect to each point in the second set. " + - "You can use the third pattern to connect points in series, like a path along the points.", - "", - "You cannot use both vectors and locations/entities, but you can mix and match locations and entities." + - "When using locations, this is a shape that can be drawn without a specific location. It will be drawn between the two given locations.", - "If using vectors, or a direction and length, the shape does require a location to be drawn at." -}) -@Examples({ - "set {_shape} to line from vector(0, 0, 0) to vector(10, 10, 10)", - "set {_shape} to a line in direction vector(1, 1, 1) and length 10", - "draw the shape of a line from vector(0, 0, 0) to vector(10, 10, 10) at player", - "", - "# note that the following does not require a location to be drawn at", - "draw the shape of a line from player to player's target", - "draw the shape of a line from player to (all players in radius 10 of player)", - "draw the shape of a line connecting {_locations::*}" -}) +@Description(""" + Creates a line shape between points, or in a direction for a given length. The length must be greater than 0. + When defining a line from points, the points can either be vectors or locations/entities. Each point in the first set will connect to each point in the second set. + You can use the third pattern to connect points in series, like a path along the points. + + You cannot use both vectors and locations/entities, but you can mix and match locations and entities. + When using locations, this is a shape that can be drawn without a specific location. It will be drawn between the two given locations. + If using vectors, or a direction and length, the shape does require a location to be drawn at. + """) +@Example("set {_shape} to line from vector(0, 0, 0) to vector(10, 10, 10)") +@Example("set {_shape} to a line in direction vector(1, 1, 1) and length 10") +@Example("draw the shape of a line from vector(0, 0, 0) to vector(10, 10, 10) at player") +@Example("") +@Example("# note that the following does not require a location to be drawn at") +@Example("draw the shape of a line from player to player's target") +@Example("draw the shape of a line from player to (all players in radius 10 of player)") +@Example("draw the shape of a line connecting {_locations::*}") @Since("1.0.0") -public class ExprLine extends SimpleExpression { - - static { - Skript.registerExpression(ExprLine.class, Shape.class, ExpressionType.COMBINED, - "[a] line[s] (from|between) %locations/entities/vectors% (to|and) %locations/entities/vectors%", - "[a] line (in [the]|from) direction %vector% [(and|[and] with) length %number%]", - "[a] line (between|connecting) %locations/entities/vectors%"); +public class ExprLine extends ShapeConstructorExpression { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprLine.class, Shape.class) + .supplier(ExprLine::new) + .addPatterns( + "[a] line[s] (from|between) %locations/entities/vectors% (to|and) %locations/entities/vectors%", + "[a] line (in [the]|from) direction %vector% [(and|[and] with) length %number%]", + "[a] line (between|connecting) %locations/entities/vectors%" + ) + .build()); } private Expression start; private Expression end; - private Expression points; - private Expression direction; private Expression length; - private int matchedPattern = 0; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { switch (matchedPattern) { case 0 -> { start = exprs[0]; @@ -88,14 +88,8 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye return true; } - private Shape attachDrawData(Shape shape) { - shape.getPointSampler().setDrawContext(new DrawData()); - return shape; - } - @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { List lines = new ArrayList<>(); switch (matchedPattern) { case 0 -> { @@ -114,7 +108,6 @@ protected Shape[] get(Event event) { if (endPoint == null || startPoint == null) { continue; } - // Use Supplier-based Line for dynamic endpoints lines.add(attachDrawData(new Line( () -> VectorConversion.toJOML(startPoint.getLocation().toVector()), () -> VectorConversion.toJOML(endPoint.getLocation().toVector()) @@ -163,7 +156,12 @@ protected Shape[] get(Event event) { return null; } } - return lines.toArray(new Shape[0]); + return lines; + } + + private Shape attachDrawData(Shape shape) { + shape.getPointSampler().setDrawContext(new DrawData()); + return shape; } @Override @@ -175,11 +173,6 @@ public boolean isSingle() { }; } - @Override - public Class getReturnType() { - return Shape.class; - } - @Override public String toString(@Nullable Event event, boolean debug) { return switch (matchedPattern) { diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRectangle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRectangle.java similarity index 62% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRectangle.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRectangle.java index 6bebf24..2724b8e 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRectangle.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRectangle.java @@ -1,52 +1,54 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Rectangle; import com.sovdee.shapes.shapes.Rectangle.Plane; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.DynamicLocation; import com.sovdee.skriptparticles.util.MathUtil; import com.sovdee.skriptparticles.util.VectorConversion; import org.bukkit.event.Event; import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Rectangle") -@Description({ - "Creates a rectangle from a length and a width, or from two corners. The length and width must be greater than 0.", - "When defining a rectangle from two corners, the corners can either be vectors or locations/entities. " + - "You cannot use both vectors and locations/entities, but you can mix and match locations and entities. " + - "When using locations, this is a shape that can be drawn without a specific location. It will be drawn between the two given locations.", - "Note that the rectangle defaults to the xz plane, or parallel to the ground, with x being width and z being length. " + - "You can change this to the xy or yz plane by using the 'xy' or 'yz'. In all cases, the first axis is length and the second is width." -}) -@Examples({ - "set {_shape} to rectangle with length 10 and width 5", - "set {_shape} to a yz rectangle from vector(0, 0, 0) to vector(10, 10, 10)", - "draw the shape of a rectangle with length 10 and width 5 at player", - "", - "# note that the following does not require a location to be drawn at", - "draw the shape of a rectangle from player to player's target" -}) +@Description(""" + Creates a rectangle from a length and a width, or from two corners. The length and width must be greater than 0. + When defining a rectangle from two corners, the corners can either be vectors or locations/entities. + You cannot use both vectors and locations/entities, but you can mix and match locations and entities. + When using locations, this is a shape that can be drawn without a specific location. It will be drawn between the two given locations. + Note that the rectangle defaults to the xz plane, or parallel to the ground, with x being width and z being length. + You can change this to the xy or yz plane by using the 'xy' or 'yz'. In all cases, the first axis is length and the second is width. + """) +@Example("set {_shape} to rectangle with length 10 and width 5") +@Example("set {_shape} to a yz rectangle from vector(0, 0, 0) to vector(10, 10, 10)") +@Example("draw the shape of a rectangle with length 10 and width 5 at player") +@Example("") +@Example("# note that the following does not require a location to be drawn at") +@Example("draw the shape of a rectangle from player to player's target") @Since("1.0.0") -public class ExprRectangle extends SimpleExpression { +public class ExprRectangle extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprRectangle.class, Shape.class, ExpressionType.COMBINED, - "[a[n]] [solid:(solid|filled)] [:xz|:xy|:yz] rectangle (with|of) length %number% and width %number%", - "[a[n]] [solid:(solid|filled)] [:xz|:xy|:yz] rectangle (from|with corners [at]) %location/entity/vector% (to|and) %location/entity/vector%" - ); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprRectangle.class, Shape.class) + .supplier(ExprRectangle::new) + .addPatterns( + "[a[n]] [solid:(solid|filled)] [:xz|:xy|:yz] rectangle (with|of) length %number% and width %number%", + "[a[n]] [solid:(solid|filled)] [:xz|:xy|:yz] rectangle (from|with corners [at]) %location/entity/vector% (to|and) %location/entity/vector%" + ) + .build()); } private Expression lengthExpr; @@ -58,7 +60,8 @@ public class ExprRectangle extends SimpleExpression { private Plane plane; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { style = parseResult.hasTag("solid") ? SamplingStyle.SURFACE : SamplingStyle.OUTLINE; this.matchedPattern = matchedPattern; if (matchedPattern == 0) { @@ -75,7 +78,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - protected @Nullable Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Shape shape; if (matchedPattern == 0) { if (lengthExpr == null || widthExpr == null) return null; @@ -100,7 +103,6 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye DynamicLocation dl2 = DynamicLocation.fromLocationEntity(corner2); if (dl1 == null || dl2 == null) return null; - // Use Supplier-based Rectangle for dynamic corners shape = new Rectangle( () -> VectorConversion.toJOML(dl1.getLocation().toVector()), () -> VectorConversion.toJOML(dl2.getLocation().toVector()), @@ -110,17 +112,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } shape.getPointSampler().setStyle(style); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRegularPolygon.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRegularPolygon.java similarity index 67% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRegularPolygon.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRegularPolygon.java index 5254a76..95b3bb2 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprRegularPolygon.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRegularPolygon.java @@ -1,45 +1,49 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.lang.util.SimpleLiteral; import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.RegularPolygon; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.shapes.sampling.SamplingStyle; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Regular Polygon") -@Description({ - "Creates a regular polygon with the given number of sides and radius. The number of sides must be at least 3. " + - "The radius must be greater than 0." -}) -@Examples({ - "set {_shape} to a regular polygon with 5 sides and radius 10", - "set {_shape} to a solid regular polygon with 6 sides and side length 3", - "draw the shape of a triangle with side length 5 at player" -}) -public class ExprRegularPolygon extends SimpleExpression { +@Description(""" + Creates a regular polygon with the given number of sides and radius. The number of sides must be at least 3 and the radius must be greater than 0. + """) +@Example("set {_shape} to a regular polygon with 5 sides and radius 10") +@Example("set {_shape} to a solid regular polygon with 6 sides and side length 3") +@Example("draw the shape of a triangle with side length 5 at player") +@Since("1.0.0") +public class ExprRegularPolygon extends ShapeConstructorExpression { // TODO: add ExprRegularPrism - static { - Skript.registerExpression(ExprRegularPolygon.class, Shape.class, ExpressionType.COMBINED, - "[a] [solid:(solid|filled)] regular polygon with %number% sides and radius %number%", - "[a] [solid:(solid|filled)] regular polygon with %number% sides and side length %number%", - "[a[n]] [solid:(solid|filled)] (3:[equilateral ]triangle|4:square|5:pentagon|6:hexagon|7:heptagon|8:octagon) with radius %number%", - "[a[n]] [solid:(solid|filled)] (3:[equilateral ]triangle|4:square|5:pentagon|6:hexagon|7:heptagon|8:octagon) with side length %number%" - ); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprRegularPolygon.class, Shape.class) + .supplier(ExprRegularPolygon::new) + .addPatterns( + "[a] [solid:(solid|filled)] regular polygon with %number% sides and radius %number%", + "[a] [solid:(solid|filled)] regular polygon with %number% sides and side length %number%", + "[a[n]] [solid:(solid|filled)] (3:[equilateral ]triangle|4:square|5:pentagon|6:hexagon|7:heptagon|8:octagon) with radius %number%", + "[a[n]] [solid:(solid|filled)] (3:[equilateral ]triangle|4:square|5:pentagon|6:hexagon|7:heptagon|8:octagon) with side length %number%" + ) + .build()); } private Expression radius; @@ -49,7 +53,8 @@ public class ExprRegularPolygon extends SimpleExpression { private int matchedPattern; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { style = parseResult.hasTag("solid") ? SamplingStyle.SURFACE : SamplingStyle.OUTLINE; this.matchedPattern = matchedPattern; switch (matchedPattern) { @@ -90,13 +95,11 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Number sides = this.sides.getSingle(event); if (sides == null) return null; - // get radius, if radius is not specified, calculate it from side length Number radius; if (matchedPattern % 2 == 0) { radius = this.radius.getSingle(event); @@ -114,17 +117,7 @@ protected Shape[] get(Event event) { RegularPolygon shape = new RegularPolygon(sides.intValue(), radius.doubleValue()); shape.getPointSampler().setStyle(style); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override @@ -138,5 +131,4 @@ public String toString(@Nullable Event event, boolean debug) { default -> "regular polygon"; }; } - } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRegularPolyhedron.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRegularPolyhedron.java new file mode 100644 index 0000000..e4670b5 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprRegularPolyhedron.java @@ -0,0 +1,78 @@ +package com.sovdee.skriptparticles.skript.shapes.constructors; + +import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.util.Kleenean; +import com.sovdee.shapes.sampling.SamplingStyle; +import com.sovdee.shapes.shapes.RegularPolyhedron; +import com.sovdee.shapes.shapes.Shape; +import com.sovdee.skriptparticles.rendering.DrawData; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Particle Regular Polyhedron") +@Description(""" + Creates a regular polyhedron shape with the given radius. The radius must be greater than 0. + Valid polyhedra are tetrahedra (4 faces), octahedra (8), dodecahedra (12), and icosahedra (20). + + Polyhedra currently do not support the particle count expression, only particle density. + """) +@Example("set {_shape} to a tetrahedron with radius 1") +@Example("set {_shape} to a solid icosahedron with radius 2") +@Example("draw the shape of a tetrahedron with radius 5 at player") +@Since("1.0.0") +public class ExprRegularPolyhedron extends ShapeConstructorExpression { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprRegularPolyhedron.class, Shape.class) + .supplier(ExprRegularPolyhedron::new) + .addPatterns("[a[n]] [outlined|:hollow|:solid] (:tetra|:octa|:icosa|:dodeca)hedron (with|of) radius %number%") + .build()); + } + + private Expression radius; + private int faces; + private SamplingStyle style; + + @Override + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] expressions, int matchedPattern, Kleenean kleenean, SkriptParser.ParseResult parseResult) { + radius = (Expression) expressions[0]; + faces = parseResult.hasTag("tetra") ? 4 : parseResult.hasTag("octa") ? 8 : parseResult.hasTag("dodeca") ? 12 : 20; + style = parseResult.hasTag("hollow") ? SamplingStyle.SURFACE : parseResult.hasTag("solid") ? SamplingStyle.FILL : SamplingStyle.OUTLINE; + + if (radius instanceof Literal literal && literal.getSingle().doubleValue() <= 0) { + Skript.error("The radius of the polyhedron must be greater than 0. (radius: " + + ((Literal) radius).getSingle().doubleValue() + ")"); + return false; + } + + return true; + } + + @Override + protected @Nullable List getShapes(Event event) { + Number r = radius.getSingle(event); + if (r == null) + return null; + RegularPolyhedron shape = new RegularPolyhedron(r.doubleValue(), faces); + shape.getPointSampler().setStyle(style); + shape.getPointSampler().setDrawContext(new DrawData()); + return List.of(shape); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "regular polyhedron with " + faces + " faces with radius " + radius.toString(event, debug); + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprSphere.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprSphere.java similarity index 56% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprSphere.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprSphere.java index 7de571a..060e8c3 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprSphere.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprSphere.java @@ -1,46 +1,49 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; -import com.sovdee.shapes.shapes.Shape; import com.sovdee.shapes.sampling.SamplingStyle; +import com.sovdee.shapes.shapes.Shape; import com.sovdee.shapes.shapes.Sphere; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Sphere") -@Description({ - "Creates a sphere shape with the given radius. The radius must be greater than 0.", - "Default style is a hollow sphere, but you can use the 'solid' tag to make it solid." -}) -@Examples({ - "set {_shape} to sphere with radius 3", - "set {_shape} to solid sphere with radius 10" -}) +@Description(""" + Creates a sphere shape with the given radius. The radius must be greater than 0. + Default style is a hollow sphere, but you can use the 'solid' tag to make it solid. + """) +@Example("set {_shape} to sphere with radius 3") +@Example("set {_shape} to solid sphere with radius 10") @Since("1.0.0") -public class ExprSphere extends SimpleExpression { +public class ExprSphere extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprSphere.class, Shape.class, ExpressionType.COMBINED, "[a] [:solid] sphere (with|of) radius %number%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprSphere.class, Shape.class) + .supplier(ExprSphere::new) + .addPatterns("[a] [:solid] sphere (with|of) radius %number%") + .build()); } private Expression radius; private boolean isSolid; @Override - public boolean init(Expression[] exprs, int matchedPattern, @NotNull Kleenean isDelayed, @NotNull ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { radius = (Expression) exprs[0]; if (radius instanceof Literal literal && literal.getSingle().doubleValue() <= 0) { Skript.error("The radius of the sphere must be greater than 0. (radius: " + literal.getSingle().doubleValue() + ")"); @@ -51,7 +54,7 @@ public boolean init(Expression[] exprs, int matchedPattern, @NotNull Kleenean } @Override - protected Shape[] get(@NotNull Event event) { + protected @Nullable List getShapes(Event event) { Number radius = this.radius.getSingle(event); if (radius == null) return null; @@ -61,22 +64,10 @@ protected Shape[] get(@NotNull Event event) { Sphere shape = new Sphere(radius.doubleValue()); if (isSolid) shape.getPointSampler().setStyle(SamplingStyle.FILL); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - @NotNull - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override - @NotNull public String toString(@Nullable Event event, boolean debug) { return "sphere with radius " + radius.toString(event, debug); } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprSphericalCap.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprSphericalCap.java similarity index 61% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprSphericalCap.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprSphericalCap.java index 81f38a9..2bdc78e 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprSphericalCap.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprSphericalCap.java @@ -1,40 +1,42 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; -import com.sovdee.shapes.shapes.Shape; import com.sovdee.shapes.sampling.SamplingStyle; +import com.sovdee.shapes.shapes.Shape; import com.sovdee.shapes.shapes.SphericalCap; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Spherical Cap") -@Description({ - "Creates a spherical cap or spherical sector shape with the given radius and cutoff angle. The radius must be greater than 0.", - "The angle must be between 0 and 180 degrees. If the angle is 180 degrees, the shape will be a sphere.", - "A spherical cap is a portion of the surface of a sphere. A spherical sector, or spherical cone, is essentially a cone with a rounded base." -}) -@Examples({ - "set {_shape} to spherical cap with radius 10 and angle 45 degrees", - "set {_shape} to a spherical sector of radius 3 and angle 90 degrees" -}) +@Description(""" + Creates a spherical cap or spherical sector shape with the given radius and cutoff angle. The radius must be greater than 0. + The angle must be between 0 and 180 degrees. If the angle is 180 degrees, the shape will be a sphere. + A spherical cap is a portion of the surface of a sphere. A spherical sector, or spherical cone, is essentially a cone with a rounded base. + """) +@Example("set {_shape} to spherical cap with radius 10 and angle 45 degrees") +@Example("set {_shape} to a spherical sector of radius 3 and angle 90 degrees") @Since("1.0.0") -public class ExprSphericalCap extends SimpleExpression { +public class ExprSphericalCap extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprSphericalCap.class, Shape.class, ExpressionType.COMBINED, - "[a] spherical (cap|:sector) (with|of) radius %number% and [cutoff] angle [of] %number% [degrees|:radians]"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprSphericalCap.class, Shape.class) + .supplier(ExprSphericalCap::new) + .addPatterns("[a] spherical (cap|:sector) (with|of) radius %number% and [cutoff] angle [of] %number% [degrees|:radians]") + .build()); } private Expression radius; @@ -43,7 +45,8 @@ public class ExprSphericalCap extends SimpleExpression { private boolean isSector = false; @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { radius = (Expression) exprs[0]; angle = (Expression) exprs[1]; isRadians = parseResult.hasTag("radians"); @@ -66,8 +69,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Number radius = this.radius.getSingle(event); Number angle = this.angle.getSingle(event); if (radius == null || angle == null) @@ -81,23 +83,11 @@ protected Shape[] get(Event event) { if (isSector) shape.getPointSampler().setStyle(SamplingStyle.FILL); shape.getPointSampler().setDrawContext(new DrawData()); - - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override public String toString(@Nullable Event event, boolean debug) { return "spherical " + (isSector ? "sector" : "cap") + " with radius " + radius.toString(event, debug) + " and angle " + angle.toString(event, debug) + (isRadians ? " radians" : " degrees"); } - } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprStar.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprStar.java similarity index 59% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprStar.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprStar.java index 4772343..0c438b8 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/constructors/ExprStar.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ExprStar.java @@ -1,36 +1,42 @@ -package com.sovdee.skriptparticles.elements.expressions.constructors; +package com.sovdee.skriptparticles.skript.shapes.constructors; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; -import com.sovdee.shapes.shapes.Shape; import com.sovdee.shapes.sampling.SamplingStyle; +import com.sovdee.shapes.shapes.Shape; import com.sovdee.shapes.shapes.Star; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.MathUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.List; +import org.skriptlang.skript.registration.SyntaxRegistry; @Name("Particle Star") -@Description({ - "Creates a star shape with the given number of points, inner radius, and outer radius. The number of points must be at least 2, and the inner and outer radii must be greater than 0.", - "Note that \"points\" in this context is referring to the tips of the star, not the number of particles." -}) -@Examples({ - "set {_shape} to star with 5 points, inner radius 1, and outer radius 2", - "draw the shape of a star with 4 points, inner radius 2, and outer radius 4 at player" -}) -public class ExprStar extends SimpleExpression { +@Description(""" + Creates a star shape with the given number of points, inner radius, and outer radius. + The number of points must be at least 2, and the inner and outer radii must be greater than 0. + Note that "points" in this context is referring to the tips of the star, not the number of particles. + """) +@Example("set {_shape} to star with 5 points, inner radius 1, and outer radius 2") +@Example("draw the shape of a star with 4 points, inner radius 2, and outer radius 4 at player") +@Since("1.0.1") +public class ExprStar extends ShapeConstructorExpression { - static { - Skript.registerExpression(ExprStar.class, Shape.class, ExpressionType.COMBINED, "[a] [:solid] star with %number% points(,| and) inner radius %number%[,] and outer radius %number%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprStar.class, Shape.class) + .supplier(ExprStar::new) + .addPatterns("[a] [:solid] star with %number% points(,| and) inner radius %number%[,] and outer radius %number%") + .build()); } private Expression points; @@ -39,7 +45,8 @@ public class ExprStar extends SimpleExpression { private boolean isSolid; @Override - public boolean init(Expression[] expressions, int i, Kleenean kleenean, ParseResult parseResult) { + @SuppressWarnings("unchecked") + public boolean initialize(Expression[] expressions, int matchedPattern, Kleenean kleenean, ParseResult parseResult) { points = (Expression) expressions[0]; innerRadius = (Expression) expressions[1]; outerRadius = (Expression) expressions[2]; @@ -65,8 +72,7 @@ public boolean init(Expression[] expressions, int i, Kleenean kleenean, Parse } @Override - @Nullable - protected Shape[] get(Event event) { + protected @Nullable List getShapes(Event event) { Number points = this.points.getSingle(event); Number innerRadius = this.innerRadius.getSingle(event); Number outerRadius = this.outerRadius.getSingle(event); @@ -81,21 +87,11 @@ protected Shape[] get(Event event) { if (isSolid) shape.getPointSampler().setStyle(SamplingStyle.SURFACE); shape.getPointSampler().setDrawContext(new DrawData()); - return new Shape[]{shape}; - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return Shape.class; + return List.of(shape); } @Override - public String toString(@Nullable Event event, boolean b) { - return "a " + (isSolid ? "solid " : "") + "star shape with " + points.toString(event, b) + " points, inner radius " + innerRadius.toString(event, b) + ", and outer radius " + outerRadius.toString(event, b); + public String toString(@Nullable Event event, boolean debug) { + return "a " + (isSolid ? "solid " : "") + "star shape with " + points.toString(event, debug) + " points, inner radius " + innerRadius.toString(event, debug) + ", and outer radius " + outerRadius.toString(event, debug); } } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ShapeConstructorExpression.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ShapeConstructorExpression.java new file mode 100644 index 0000000..d8b7ae9 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/constructors/ShapeConstructorExpression.java @@ -0,0 +1,93 @@ +package com.sovdee.skriptparticles.skript.shapes.constructors; + +import ch.njol.skript.Skript; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.expressions.base.SectionExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.TriggerItem; +import ch.njol.util.Kleenean; +import com.sovdee.shapes.shapes.Shape; +import com.sovdee.skriptparticles.util.SkriptUtil; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Base class for all the shape constructors + */ +public abstract class ShapeConstructorExpression extends SectionExpression { + + private boolean hasSection = false; + private final ThreadLocal threadShape = new ThreadLocal<>(); + + @Override + public boolean init( + Expression[] expressions, int matchedPattern, Kleenean hasDelayBefore, ParseResult parseResult, + @Nullable SectionNode sectionNode, @Nullable List list + ) { + if (!initialize(expressions, matchedPattern, hasDelayBefore, parseResult)) + return false; + + if (SkriptUtil.getCurrentSectionExpression(ShapeConstructorExpression.class, getParser()) != null) { + Skript.error("Shapes cannot be constructed within other shape's constructor sections!"); + return false; + } + + if (sectionNode != null) { + getParser().setHasDelayBefore(Kleenean.FALSE); + loadCode(sectionNode); + if (!getParser().getHasDelayBefore().isFalse()) { + Skript.error("Delays can't be used within a shape creation section"); + return false; + } + getParser().setHasDelayBefore(hasDelayBefore); + hasSection = true; + } + return false; + } + + protected abstract boolean initialize(Expression[] expressions, int matchedPattern, Kleenean hasDelayBefore, ParseResult parseResult); + + @Override + protected Shape @Nullable [] get(Event event) { + List shapes = getShapes(event); + if (shapes == null) + return null; + // run section code to modify shapes + if (hasSection) { + for (Shape shape : shapes) { + threadShape.set(shape); + runSection(event); + } + threadShape.remove(); + } + return shapes.toArray(new Shape[0]); + } + + /** + * Returns the initially constructed shapes for modification by the section, if it exists. + * @param event The event context. + * @return The created shape. + */ + protected abstract @Nullable List getShapes(Event event); + + /** + * @return The shape currently being constructed by this syntax on this thread. + */ + public Shape getCurrentShape() { + return threadShape.get(); + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return Shape.class; + } + +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffRotateShape.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/effects/EffRotateShape.java similarity index 72% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffRotateShape.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/effects/EffRotateShape.java index 031b798..313f888 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffRotateShape.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/effects/EffRotateShape.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.effects; +package com.sovdee.skriptparticles.skript.shapes.effects; import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; @@ -9,7 +9,10 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Kleenean; -import com.sovdee.skriptparticles.elements.sections.DrawShapeEffectSection.DrawEvent; +import com.sovdee.skriptparticles.skript.drawing.sections.DrawShapeEffectSection; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import com.sovdee.skriptparticles.skript.drawing.sections.DrawShapeEffectSection.DrawEvent; import com.sovdee.shapes.shapes.Shape; import org.bukkit.event.Event; import org.bukkit.util.Vector; @@ -17,6 +20,13 @@ import org.joml.Quaterniond; import org.joml.Quaternionf; +/** + * Skript effect that rotates one or more shapes around an axis by an angle, or applies a quaternion rotation. + * Supports named cardinal axes (x, y, z), arbitrary vector axes, local (relative) rotation, and radians/degrees. + * When used inside a {@link DrawShapeEffectSection.DrawEvent DrawEvent} + * section without an explicit shape argument, it operates on the currently-being-drawn shape. + * See {@code @Name}, {@code @Description}, {@code @Examples}, and {@code @Since} for Skript-facing documentation. + */ @Name("Rotate Shape") @Description({ "Rotates shapes around a given axis by a given angle. The axis can be specified as a vector or as a single axis (x, y, or z). " + @@ -32,13 +42,16 @@ @Since("1.0.0") public class EffRotateShape extends Effect { - static { - Skript.registerEffect(EffRotateShape.class, - "rotate shape[s] %shapes% around [relative:(relative|local)] (v:%-vector%|((:x|:y|:z)(-| )axis)) by %number% [degrees|:radians]", - "rotate shape[s] %shapes% (by|with) [rotation] %quaternion%", - "rotate [drawn] shape[s] around [relative:(relative|local)] (v:%-vector%|((:x|:y|:z)(-| )axis)) by %number% [degrees|:radians]", - "rotate [drawn] shape[s] (by|with) [rotation] %quaternion%" - ); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EFFECT, SyntaxInfo.builder(EffRotateShape.class) + .supplier(EffRotateShape::new) + .addPatterns( + "rotate shape[s] %shapes% around [relative:(relative|local)] (v:%-vector%|((:x|:y|:z)(-| )axis)) by %number% [degrees|:radians]", + "rotate shape[s] %shapes% (by|with) [rotation] %quaternion%", + "rotate [drawn] shape[s] around [relative:(relative|local)] (v:%-vector%|((:x|:y|:z)(-| )axis)) by %number% [degrees|:radians]", + "rotate [drawn] shape[s] (by|with) [rotation] %quaternion%" + ) + .build()); } private Expression shapes; @@ -51,6 +64,10 @@ public class EffRotateShape extends Effect { private boolean isRadians = false; private boolean isAxisAngle = false; + /** + * Determines whether drawn-shapes or explicit shapes are used, resolves axis/quaternion mode, + * and validates that the effect is inside a DrawEvent section when no explicit shape is given. + */ @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { int offset = 0; @@ -80,6 +97,10 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye return true; } + /** + * Applies the rotation to each target shape; uses premul for global rotations and mul for + * relative/local ones. + */ @Override protected void execute(Event event) { Quaterniond rotation; @@ -130,6 +151,9 @@ protected void execute(Event event) { } } + /** + * {@inheritDoc} + */ @Override public String toString(@Nullable Event event, boolean debug) { if (rotation != null) diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffToggleAxes.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/effects/EffToggleAxes.java similarity index 66% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffToggleAxes.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/effects/EffToggleAxes.java index b8026f5..91668c9 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/effects/EffToggleAxes.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/effects/EffToggleAxes.java @@ -1,6 +1,5 @@ -package com.sovdee.skriptparticles.elements.effects; +package com.sovdee.skriptparticles.skript.shapes.effects; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; @@ -9,11 +8,18 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.shapes.DrawData; +import com.sovdee.skriptparticles.rendering.DrawData; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +/** + * Skript effect that shows or hides the local and/or global debug axes of one or more shapes. + * Intended for debugging purposes; toggles the corresponding flag on each shape's {@link DrawData DrawData}. + * Documented from the Skript side via {@code @Name}, {@code @Description}, {@code @Examples}, and {@code @Since}. + */ @Name("Toggle Axes") @Description({ "Toggles the visibility of the local and/or global axes of a shape.", @@ -29,8 +35,11 @@ @Since("1.0.0") public class EffToggleAxes extends Effect { - static { - Skript.registerEffect(EffToggleAxes.class, "(:show|:hide) [:local] [and] [:global] axes of [shape[s]] %shapes%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EFFECT, SyntaxInfo.builder(EffToggleAxes.class) + .supplier(EffToggleAxes::new) + .addPatterns("(:show|:hide) [:local] [and] [:global] axes of [shape[s]] %shapes%") + .build()); } private Expression shape; @@ -38,6 +47,9 @@ public class EffToggleAxes extends Effect { private boolean globalFlag = false; private boolean showFlag = false; + /** + * {@inheritDoc} + */ @Override public boolean init(Expression[] expressions, int i, Kleenean kleenean, SkriptParser.ParseResult parseResult) { shape = (Expression) expressions[0]; @@ -47,6 +59,9 @@ public boolean init(Expression[] expressions, int i, Kleenean kleenean, Skrip return true; } + /** + * Applies the show/hide flags to the local and/or global axes of each shape's DrawData. + */ @Override protected void execute(Event event) { Shape[] shapes = shape.getArray(event); @@ -59,6 +74,9 @@ protected void execute(Event event) { } } + /** + * {@inheritDoc} + */ @Override public String toString(@Nullable Event event, boolean debug) { return (showFlag ? "show" : "hide") + (globalFlag ? "global" : "") + (globalFlag && localFlag ? " and " : "") + diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprCurrentShape.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprCurrentShape.java new file mode 100644 index 0000000..3ec4007 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprCurrentShape.java @@ -0,0 +1,72 @@ +package com.sovdee.skriptparticles.skript.shapes.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import com.sovdee.shapes.shapes.Shape; +import com.sovdee.skriptparticles.skript.drawing.expressions.ExprDrawnShapes; +import com.sovdee.skriptparticles.skript.drawing.sections.DrawShapeEffectSection; +import com.sovdee.skriptparticles.skript.shapes.constructors.ShapeConstructorExpression; +import com.sovdee.skriptparticles.util.SkriptUtil; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +public class ExprCurrentShape extends SimpleExpression { + + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.builder(ExprCurrentShape.class, Shape.class) + .addPattern("[the] [current] shape") + .supplier(ExprCurrentShape::new) + .build()); + } + + private ShapeConstructorExpression section; + private ExprDrawnShapes eventExpr; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + section = SkriptUtil.getCurrentSectionExpression(ShapeConstructorExpression.class, getParser()); + if (section == null) { + if (getParser().isCurrentEvent(DrawShapeEffectSection.DrawEvent.class)) { + eventExpr = new ExprDrawnShapes(); + } else { + Skript.error("The 'current shape' expression can only be used inside of a shape construction section. Use 'drawn shape' for draw shape sections."); + return false; + } + } + return true; + } + + @Override + protected Shape @Nullable [] get(Event event) { + Shape shape; + if (section != null) { + shape = section.getCurrentShape(); + } else { + shape = eventExpr.getSingle(event); + } + return new Shape[]{shape}; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return Shape.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "the current shape"; + } + +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprRotation.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprRotation.java similarity index 75% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprRotation.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprRotation.java index a270f41..91045db 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprRotation.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprRotation.java @@ -1,21 +1,27 @@ -package com.sovdee.skriptparticles.elements.expressions; +package com.sovdee.skriptparticles.skript.shapes.expressions; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.skriptparticles.util.Quaternion; import org.bukkit.event.Event; import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; +/** + * Skript expression that constructs a {@link com.sovdee.skriptparticles.util.Quaternion} representing a rotation. + * Supports two forms: axis-angle (around a vector by a given angle in degrees or radians) and + * vector-to-vector (the shortest rotation from one direction to another). + * Documented from the Skript side via {@code @Name}, {@code @Description}, {@code @Examples}, and {@code @Since}. + */ @Name("Rotation") @Description("Describes a rotation around a vector by a given angle, or from one vector to another. An alternative to the `axisAngle` and `quaternion` functions. Returns a quaternion.") @Examples({ @@ -27,10 +33,14 @@ @Since("1.0.0") public class ExprRotation extends SimpleExpression { - static { - Skript.registerExpression(ExprRotation.class, Quaternion.class, ExpressionType.COMBINED, - "[the|a] rotation (from|around) [the] [vector] %vector% (with|by) [[the|an] angle [of]] %number% [degrees|:radians]", - "[the|a] rotation (from|between) %vector% (to|and) %vector%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprRotation.class, Quaternion.class) + .supplier(ExprRotation::new) + .addPatterns( + "[the|a] rotation (from|around) [the] [vector] %vector% (with|by) [[the|an] angle [of]] %number% [degrees|:radians]", + "[the|a] rotation (from|between) %vector% (to|and) %vector%" + ) + .build()); } @Nullable diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprShapeCopy.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprShapeCopy.java similarity index 73% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprShapeCopy.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprShapeCopy.java index a85e9a1..89ff69c 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/ExprShapeCopy.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/expressions/ExprShapeCopy.java @@ -1,18 +1,18 @@ -package com.sovdee.skriptparticles.elements.expressions; +package com.sovdee.skriptparticles.skript.shapes.expressions; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; import com.sovdee.shapes.shapes.Shape; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import org.bukkit.event.Event; -import org.checkerframework.checker.nullness.qual.NonNull; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -27,8 +27,12 @@ @Since("1.0.0") public class ExprShapeCopy extends SimpleExpression { - static { - Skript.registerExpression(ExprShapeCopy.class, Shape.class, ExpressionType.SIMPLE, "[a] shape cop(y|ies) of %shapes%"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprShapeCopy.class, Shape.class) + .supplier(ExprShapeCopy::new) + .addPatterns("[a] shape cop(y|ies) of %shapes%") + .priority(SyntaxInfo.SIMPLE) + .build()); } private Expression shapeExpr; @@ -40,7 +44,7 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean kl } @Override - protected Shape[] get(@NonNull Event event) { + protected Shape[] get(@NotNull Event event) { Shape[] shape = shapeExpr.getArray(event); if (shape.length == 0) diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprHelixWindingRate.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprHelixWindingRate.java similarity index 84% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprHelixWindingRate.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprHelixWindingRate.java index d1a501c..e7f5a76 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprHelixWindingRate.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprHelixWindingRate.java @@ -1,12 +1,14 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.Helix; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.shapes.shapes.Shape; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -25,8 +27,11 @@ @Since("1.0.0") public class ExprHelixWindingRate extends SimplePropertyExpression { - static { - register(ExprHelixWindingRate.class, Number.class, "winding rate", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprHelixWindingRate.class, Number.class, "winding rate", "shapes", false) + .supplier(ExprHelixWindingRate::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeCutoffAngle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeCutoffAngle.java similarity index 83% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeCutoffAngle.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeCutoffAngle.java index 30b1373..4769a75 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeCutoffAngle.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeCutoffAngle.java @@ -1,12 +1,14 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.CutoffShape; +import org.skriptlang.skript.registration.SyntaxRegistry; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -24,8 +26,11 @@ @Since("1.0.0") public class ExprShapeCutoffAngle extends SimplePropertyExpression { - static { - register(ExprShapeCutoffAngle.class, Number.class, "cutoff angle", "cutoffshapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeCutoffAngle.class, Number.class, "cutoff angle", "cutoffshapes", false) + .supplier(ExprShapeCutoffAngle::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeLWH.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeLWH.java similarity index 87% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeLWH.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeLWH.java index f9b2d3d..fc7a86a 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeLWH.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeLWH.java @@ -1,12 +1,14 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import ch.njol.skript.lang.Expression; +import org.skriptlang.skript.registration.SyntaxRegistry; import ch.njol.skript.lang.SkriptParser; import ch.njol.util.Kleenean; import com.sovdee.shapes.shapes.LWHShape; @@ -28,8 +30,11 @@ @Since("1.0.0") public class ExprShapeLWH extends SimplePropertyExpression { - static { - register(ExprShapeLWH.class, Number.class, "shape (:length|:width|:height)", "lwhshapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeLWH.class, Number.class, "shape (:length|:width|:height)", "lwhshapes", false) + .supplier(ExprShapeLWH::new) + .build()); } private int lwh; diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeLocations.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeLocations.java similarity index 76% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeLocations.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeLocations.java index 72b56f2..9a86051 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeLocations.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeLocations.java @@ -1,13 +1,13 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.util.Direction; import ch.njol.util.Kleenean; @@ -15,8 +15,8 @@ import com.sovdee.skriptparticles.util.VectorConversion; import org.bukkit.Location; import org.bukkit.event.Event; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -35,8 +35,11 @@ @Since("1.0.0") public class ExprShapeLocations extends SimpleExpression { - static { - Skript.registerExpression(ExprShapeLocations.class, Location.class, ExpressionType.COMBINED, "(particle|point) locations of %shapes% [[centered] %direction% %location%]"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprShapeLocations.class, Location.class) + .supplier(ExprShapeLocations::new) + .addPatterns("(particle|point) locations of %shapes% [[centered] %direction% %location%]") + .build()); } private Expression shapeExpr; @@ -73,12 +76,12 @@ public boolean isSingle() { } @Override - public @NonNull Class getReturnType() { + public @NotNull Class getReturnType() { return Location.class; } @Override - public @NonNull String toString(@Nullable Event event, boolean debug) { + public @NotNull String toString(@Nullable Event event, boolean debug) { return "Locations of shapes"; } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeModifiers.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeModifiers.java new file mode 100644 index 0000000..ff906ca --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeModifiers.java @@ -0,0 +1,133 @@ +package com.sovdee.skriptparticles.skript.shapes.properties; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import com.sovdee.shapes.modifiers.PointModifier; +import com.sovdee.shapes.shapes.Shape; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +@Name("Shape Modifiers") +@Description({ + "The modifiers of a shape. Modifiers transform geometry (taper, twist, wave) or render properties (gradients, motion).", + "Geometry modifiers are cached; render modifiers run per-frame.", + "SET replaces all modifiers. ADD appends a modifier. REMOVE removes a specific modifier. DELETE/RESET clears all modifiers." +}) +@Examples({ + "add taper from 1 to 0 to modifiers of {_shape}", + "add a twist of 90 degrees to modifiers of {_helix}", + "add gradient from red to blue along the y axis to modifiers of {_shape}", + "add motion modifier counterclockwise to modifiers of {_shape}", + "set modifiers of {_shape} to gradient from red to blue along the y axis", + "delete modifiers of {_shape}", +}) +@Since("2.0.0") +@SuppressWarnings("rawtypes") +public class ExprShapeModifiers extends SimpleExpression { + + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprShapeModifiers.class, PointModifier.class) + .supplier(ExprShapeModifiers::new) + .addPatterns( + "[point] modifiers of %shapes%", + "%shapes%'[s] [point] modifiers" + ) + .build()); + } + + private Expression shapes; + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + this.shapes = (Expression) exprs[0]; + return true; + } + + @Override + protected PointModifier @Nullable [] get(Event event) { + Shape[] shapeArr = shapes.getArray(event); + if (shapeArr.length == 0) return new PointModifier[0]; + List> mods = new ArrayList<>(); + for (Shape shape : shapeArr) { + mods.addAll(shape.getPointSampler().getModifiers()); + } + return mods.toArray(new PointModifier[0]); + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (mode) { + case SET, REMOVE, ADD -> new Class[]{PointModifier.class}; + case DELETE -> new Class[0]; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + Shape[] shapeArr = shapes.getArray(event); + switch (mode) { + case SET -> { + if (delta == null) return; + for (Shape shape : shapeArr) { + shape.getPointSampler().clearModifiers(); + for (Object o : delta) { + if (o instanceof PointModifier mod) + shape.getPointSampler().addModifier(mod.clone()); + } + } + } + case ADD -> { + if (delta == null) return; + for (Shape shape : shapeArr) { + for (Object o : delta) { + if (o instanceof PointModifier mod) + shape.getPointSampler().addModifier(mod); + } + } + } + case REMOVE -> { + if (delta == null) return; + for (Shape shape : shapeArr) { + for (Object o : delta) { + if (o instanceof PointModifier mod) + shape.getPointSampler().removeModifier(mod); + } + } + } + case DELETE, RESET -> { + for (Shape shape : shapeArr) + shape.getPointSampler().clearModifiers(); + } + } + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + @SuppressWarnings("rawtypes") + public Class getReturnType() { + return PointModifier.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "modifiers of " + shapes.toString(event, debug); + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeNormal.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeNormal.java similarity index 86% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeNormal.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeNormal.java index 96f4b97..d7f8c54 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeNormal.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeNormal.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -8,6 +8,7 @@ import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.Shape; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.skriptparticles.util.Quaternion; import com.sovdee.skriptparticles.util.VectorConversion; import org.joml.Quaterniond; @@ -29,8 +30,11 @@ @Since("1.0.0") public class ExprShapeNormal extends SimplePropertyExpression { - static { - PropertyExpression.register(ExprShapeNormal.class, Vector.class, "normal [vector]", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeNormal.class, Vector.class, "normal [vector]", "shapes", false) + .supplier(ExprShapeNormal::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeOffset.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeOffset.java similarity index 84% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeOffset.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeOffset.java index d92597c..fee50bc 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeOffset.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeOffset.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -7,6 +7,7 @@ import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.Shape; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.skriptparticles.util.VectorConversion; import org.bukkit.event.Event; import org.bukkit.util.Vector; @@ -26,8 +27,11 @@ }) public class ExprShapeOffset extends SimplePropertyExpression { - static { - PropertyExpression.register(ExprShapeOffset.class, Vector.class, "offset [vector]", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeOffset.class, Vector.class, "offset [vector]", "shapes", false) + .supplier(ExprShapeOffset::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeOrientation.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeOrientation.java similarity index 86% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeOrientation.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeOrientation.java index 4eaeda5..7dc94f2 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeOrientation.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeOrientation.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -8,6 +8,7 @@ import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.Shape; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.skriptparticles.util.Quaternion; import org.joml.Quaterniond; import org.bukkit.event.Event; @@ -29,8 +30,11 @@ @Since("1.0.0") public class ExprShapeOrientation extends SimplePropertyExpression { - static { - PropertyExpression.register(ExprShapeOrientation.class, Quaternion.class, "orientation", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeOrientation.class, Quaternion.class, "orientation", "shapes", false) + .supplier(ExprShapeOrientation::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeParticle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeParticle.java similarity index 82% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeParticle.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeParticle.java index 458dd38..cc5dc11 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeParticle.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeParticle.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -8,8 +8,9 @@ import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.Shape; -import com.sovdee.skriptparticles.particles.Particle; -import com.sovdee.skriptparticles.shapes.DrawData; +import org.skriptlang.skript.registration.SyntaxRegistry; +import com.sovdee.skriptparticles.rendering.Particle; +import com.sovdee.skriptparticles.rendering.DrawData; import com.sovdee.skriptparticles.util.ParticleUtil; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -34,8 +35,11 @@ @Since("1.0.0") public class ExprShapeParticle extends SimplePropertyExpression { - static { - PropertyExpression.register(ExprShapeParticle.class, Particle.class, "[custom] particle", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeParticle.class, Particle.class, "[custom] particle", "shapes", false) + .supplier(ExprShapeParticle::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeParticleDensity.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeParticleDensity.java similarity index 92% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeParticleDensity.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeParticleDensity.java index 59661cc..4cad510 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeParticleDensity.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeParticleDensity.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -7,6 +7,7 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.skriptlang.skript.registration.SyntaxRegistry; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; import ch.njol.util.Kleenean; @@ -39,10 +40,13 @@ @Since("1.0.0") public class ExprShapeParticleDensity extends SimplePropertyExpression { - static { + public static void register(SyntaxRegistry registry) { // most shapes kinda disregard the particle count, but it's still useful for some // every shape responds to the particle density property pretty well though - PropertyExpression.register(ExprShapeParticleDensity.class, Number.class, "particle (:density|:count)", "shapes"); + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeParticleDensity.class, Number.class, "particle (:density|:count)", "shapes", false) + .supplier(ExprShapeParticleDensity::new) + .build()); } private boolean isDensity; diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapePoints.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapePoints.java similarity index 82% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapePoints.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapePoints.java index dff9e4a..ea5302d 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapePoints.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapePoints.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; @@ -6,6 +6,7 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.lang.Expression; +import org.skriptlang.skript.registration.SyntaxRegistry; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Kleenean; import com.sovdee.shapes.shapes.Shape; @@ -31,8 +32,11 @@ @Since("1.0.0") public class ExprShapePoints extends PropertyExpression { - static { - register(ExprShapePoints.class, Vector.class, "points", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapePoints.class, Vector.class, "points", "shapes", false) + .supplier(ExprShapePoints::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeRadius.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeRadius.java similarity index 85% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeRadius.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeRadius.java index 53e633e..6275bf5 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeRadius.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeRadius.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -8,6 +8,7 @@ import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.RadialShape; +import org.skriptlang.skript.registration.SyntaxRegistry; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -25,8 +26,11 @@ @Since("1.0.0") public class ExprShapeRadius extends SimplePropertyExpression { - static { - PropertyExpression.register(ExprShapeRadius.class, Number.class, "radius", "radialshapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeRadius.class, Number.class, "radius", "radialshapes", false) + .supplier(ExprShapeRadius::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeRelativeAxis.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeRelativeAxis.java similarity index 82% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeRelativeAxis.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeRelativeAxis.java index cf3c0ff..cac942a 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeRelativeAxis.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeRelativeAxis.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; @@ -6,6 +6,7 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.skriptlang.skript.registration.SyntaxRegistry; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; import ch.njol.util.Kleenean; @@ -30,8 +31,11 @@ @Since("1.0.0") public class ExprShapeRelativeAxis extends SimplePropertyExpression { - static { - PropertyExpression.register(ExprShapeRelativeAxis.class, Vector.class, "(relative|local) (:x|:y|:z)(-| )axis", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeRelativeAxis.class, Vector.class, "(relative|local) (:x|:y|:z)(-| )axis", "shapes", false) + .supplier(ExprShapeRelativeAxis::new) + .build()); } private int axis; diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeScale.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeScale.java similarity index 86% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeScale.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeScale.java index 89beadb..77d45b8 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeScale.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeScale.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -8,6 +8,7 @@ import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.Shape; +import org.skriptlang.skript.registration.SyntaxRegistry; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -26,8 +27,11 @@ @Since("1.0.0") public class ExprShapeScale extends SimplePropertyExpression { - static { - PropertyExpression.register(ExprShapeScale.class, Number.class, "shape scale", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeScale.class, Number.class, "shape scale", "shapes", false) + .supplier(ExprShapeScale::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeSideLength.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeSideLength.java similarity index 83% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeSideLength.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeSideLength.java index 7f7c56e..1d91a2c 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeSideLength.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeSideLength.java @@ -1,12 +1,14 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.PolyShape; +import org.skriptlang.skript.registration.SyntaxRegistry; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -25,8 +27,11 @@ @Since("1.0.0") public class ExprShapeSideLength extends SimplePropertyExpression { - static { - register(ExprShapeSideLength.class, Number.class, "side length", "polyshapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeSideLength.class, Number.class, "side length", "polyshapes", false) + .supplier(ExprShapeSideLength::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeSides.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeSides.java similarity index 83% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeSides.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeSides.java index f93736a..2f0831c 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeSides.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeSides.java @@ -1,12 +1,14 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.PolyShape; +import org.skriptlang.skript.registration.SyntaxRegistry; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -25,8 +27,11 @@ @Since("1.0.0") public class ExprShapeSides extends SimplePropertyExpression { - static { - register(ExprShapeSides.class, Integer.class, "side(s| count)", "polyshapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeSides.class, Integer.class, "side(s| count)", "polyshapes", false) + .supplier(ExprShapeSides::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeStyle.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeStyle.java similarity index 81% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeStyle.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeStyle.java index f90c45a..1ee1456 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprShapeStyle.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprShapeStyle.java @@ -1,4 +1,4 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -7,6 +7,7 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.shapes.sampling.SamplingStyle; import com.sovdee.shapes.shapes.Shape; import org.bukkit.event.Event; @@ -25,8 +26,11 @@ @Since("1.0.0") public class ExprShapeStyle extends SimplePropertyExpression { - static { - PropertyExpression.register(ExprShapeStyle.class, SamplingStyle.class, "style", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprShapeStyle.class, SamplingStyle.class, "style", "shapes", false) + .supplier(ExprShapeStyle::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprStarPoints.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprStarPoints.java similarity index 83% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprStarPoints.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprStarPoints.java index 94f30eb..ea04ffa 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprStarPoints.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprStarPoints.java @@ -1,12 +1,14 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.shapes.shapes.Star; +import org.skriptlang.skript.registration.SyntaxRegistry; import com.sovdee.shapes.shapes.Shape; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -24,8 +26,11 @@ @Since("1.0.1") public class ExprStarPoints extends SimplePropertyExpression { - static { - register(ExprStarPoints.class, Number.class, "star points", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprStarPoints.class, Number.class, "star points", "shapes", false) + .supplier(ExprStarPoints::new) + .build()); } @Override diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprStarRadii.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprStarRadii.java similarity index 87% rename from skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprStarRadii.java rename to skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprStarRadii.java index 5108987..23f4dc8 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/elements/expressions/properties/ExprStarRadii.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/skript/shapes/properties/ExprStarRadii.java @@ -1,12 +1,14 @@ -package com.sovdee.skriptparticles.elements.expressions.properties; +package com.sovdee.skriptparticles.skript.shapes.properties; import ch.njol.skript.classes.Changer; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.expressions.base.SimplePropertyExpression; import ch.njol.skript.lang.Expression; +import org.skriptlang.skript.registration.SyntaxRegistry; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Kleenean; import com.sovdee.shapes.shapes.Shape; @@ -28,8 +30,11 @@ @Since("1.0.1") public class ExprStarRadii extends SimplePropertyExpression { - static { - register(ExprStarRadii.class, Number.class, "(:inner|outer) radius", "shapes"); + public static void register(SyntaxRegistry registry) { + registry.register(SyntaxRegistry.EXPRESSION, + PropertyExpression.infoBuilder(ExprStarRadii.class, Number.class, "(:inner|outer) radius", "shapes", false) + .supplier(ExprStarRadii::new) + .build()); } private boolean isInner; diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/DynamicLocation.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/DynamicLocation.java index 1eb82a1..7f87f1e 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/DynamicLocation.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/DynamicLocation.java @@ -3,7 +3,8 @@ import ch.njol.skript.util.Direction; import org.bukkit.Location; import org.bukkit.entity.Entity; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Contract; /** @@ -22,7 +23,7 @@ public class DynamicLocation { * Creates a dynamic location with the given entity * @param entity the entity to create the dynamic location from */ - public DynamicLocation(Entity entity) { + public DynamicLocation(@NotNull Entity entity) { this.entity = entity; } @@ -49,7 +50,7 @@ public DynamicLocation(Location location, @Nullable Direction direction) { * @param entity the entity to create the dynamic location from * @param direction the direction to create the dynamic location from */ - public DynamicLocation(Entity entity, @Nullable Direction direction) { + public DynamicLocation(@NotNull Entity entity, @Nullable Direction direction) { this.entity = entity; this.direction = direction; } @@ -157,7 +158,7 @@ public Direction getDirection() { * * @param direction the direction to set the dynamic location to */ - public void setDirection(Direction direction) { + public void setDirection(@Nullable Direction direction) { this.direction = direction; } @@ -179,6 +180,15 @@ public DynamicLocation clone() { return new DynamicLocation(this); } + /** + * Compares this {@code DynamicLocation} to another object for equality. Two instances are + * equal when they reference the same entity (by {@link Entity#equals}) or the same location + * (by {@link Location#equals}), and both have equal directions (or both have no direction). + * + * @param obj the object to compare against + * @return {@code true} if {@code obj} is a {@code DynamicLocation} with the same entity or + * location and the same direction + */ @Override public boolean equals(Object obj) { if (!(obj instanceof DynamicLocation dynamicLocation)) diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/MathUtil.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/MathUtil.java index c92e297..59aad07 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/MathUtil.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/MathUtil.java @@ -5,9 +5,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; public class MathUtil { public static final double EPSILON = 0.0001; @@ -16,8 +14,8 @@ public static double clamp(double value, double min, double max) { return Math.clamp(value, min, max); } - public static Set calculateLine(Vector start, Vector end, double particleDensity) { - Set points = new LinkedHashSet<>(); + public static List calculateLine(Vector start, Vector end, double particleDensity) { + List points = new ArrayList<>(); Vector direction = end.clone().subtract(start); double length = direction.length(); double step = length / Math.round(length / particleDensity); @@ -29,12 +27,12 @@ public static Set calculateLine(Vector start, Vector end, double particl return points; } - public static List> batch(Collection toDraw, double millisecondsPerPoint) { - List> batches = new ArrayList<>(); + public static List> batch(Collection toDraw, double millisecondsPerPoint) { + List> batches = new ArrayList<>(); double totalDuration = 0; - Iterator pointsIterator = toDraw.iterator(); + Iterator pointsIterator = toDraw.iterator(); while (pointsIterator.hasNext()) { - List batch = new ArrayList<>(); + List batch = new ArrayList<>(); while (totalDuration < 50 && pointsIterator.hasNext()) { totalDuration += millisecondsPerPoint; batch.add(pointsIterator.next()); diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/ParticleUtil.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/ParticleUtil.java index 6e791ef..ac37170 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/ParticleUtil.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/ParticleUtil.java @@ -9,24 +9,21 @@ import org.bukkit.util.Vector; import java.util.Collection; -import java.util.Set; +import java.util.List; -/* - * Thanks to ShaneBee at SkBee for the original code. - */ public class ParticleUtil { private static final ParticleBuilder Y_AXIS = new ParticleBuilder(Particle.DUST).data(new DustOptions(DyeColor.LIME.getColor(), 0.5f)); private static final ParticleBuilder X_AXIS = new ParticleBuilder(Particle.DUST).data(new DustOptions(DyeColor.RED.getColor(), 0.5f)); private static final ParticleBuilder Z_AXIS = new ParticleBuilder(Particle.DUST).data(new DustOptions(DyeColor.BLUE.getColor(), 0.5f)); - public static com.sovdee.skriptparticles.particles.Particle getDefaultParticle() { - return (com.sovdee.skriptparticles.particles.Particle) new com.sovdee.skriptparticles.particles.Particle(Particle.FLAME).count(1).extra(0); + public static com.sovdee.skriptparticles.rendering.Particle getDefaultParticle() { + return new com.sovdee.skriptparticles.rendering.Particle(Particle.FLAME).count(1).extra(0); } public static void drawAxes(Location location, Quaternion orientation, Collection recipients) { - Set yAxis = MathUtil.calculateLine(new Vector(0, 0, 0), new Vector(0, 1, 0), 0.2); - Set xAxis = MathUtil.calculateLine(new Vector(0, 0, 0), new Vector(1, 0, 0), 0.2); - Set zAxis = MathUtil.calculateLine(new Vector(0, 0, 0), new Vector(0, 0, 1), 0.2); + List yAxis = MathUtil.calculateLine(new Vector(0, 0, 0), new Vector(0, 1, 0), 0.2); + List xAxis = MathUtil.calculateLine(new Vector(0, 0, 0), new Vector(1, 0, 0), 0.2); + List zAxis = MathUtil.calculateLine(new Vector(0, 0, 0), new Vector(0, 0, 1), 0.2); yAxis = orientation.transform(yAxis); xAxis = orientation.transform(xAxis); diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/Point.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/Point.java index 796df84..ae98bef 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/Point.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/Point.java @@ -3,18 +3,17 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.util.Vector; -import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; public class Point { private final T rawValue; private final Class type; - private boolean isDynamic; + private final boolean isDynamic; public Point(T value, Class type) { this(value, type, false); @@ -37,20 +36,17 @@ public static List> of(List values) { @Nullable @Contract("!null -> !null") public static Point of(@Nullable Object value) { - if (value == null) - return null; - - if (value instanceof Vector v) { - return Point.of(v); - } else if (value instanceof Entity v) { - return Point.of(v); - } else if (value instanceof Location v) { - return Point.of(v); - } else if (value instanceof DynamicLocation v) { - return Point.of(v); - } - assert false; - return null; + return switch (value) { + case null -> null; + case Vector v -> Point.of(v); + case Entity v -> Point.of(v); + case Location v -> Point.of(v); + case DynamicLocation v -> Point.of(v); + default -> { + assert false; + yield null; + } + }; } public static Point of(Vector value) { @@ -147,4 +143,4 @@ public boolean isDynamic() { return isDynamic; } -} \ No newline at end of file +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/Quaternion.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/Quaternion.java index 0310eac..eff2475 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/Quaternion.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/Quaternion.java @@ -4,9 +4,7 @@ import org.joml.Quaternionf; import org.joml.Vector3f; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * Helper class for JOML's Quaternionf class @@ -46,18 +44,10 @@ public Quaternion(Quaternion quaternion) { } public List transform(List vectors) { - vectors.replaceAll(this::transform); + vectors.forEach(this::transform); return vectors; } - public Set transform(Set vectors) { - Set newVectors = new HashSet<>(); - for (Vector vector : vectors) { - newVectors.add(this.transform(vector)); - } - return newVectors; - } - public Vector transform(Vector vector) { Vector3f vector3f = new Vector3f((float) vector.getX(), (float) vector.getY(), (float) vector.getZ()); vector3f = this.transform(vector3f); diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/ReflectionUtils.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/ReflectionUtils.java deleted file mode 100644 index 68d78ff..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/ReflectionUtils.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sovdee.skriptparticles.util; - -import org.bukkit.Bukkit; -import org.checkerframework.checker.nullness.qual.Nullable; - -/* - * Thanks to ShaneBee at SkBee for the original code. - * This is meant to fill the gap when SkBee isn't installed. - */ - -public class ReflectionUtils { - private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackage().getName(); - private static final boolean DEBUG = false; //SkBee.getPlugin().getPluginConfig().SETTINGS_DEBUG; - - @Nullable - public static Class getOBCClass(String obcClassString) { - String name = CRAFTBUKKIT_PACKAGE + "." + obcClassString; - try { - return Class.forName(name); - } catch (ClassNotFoundException e) { - if (DEBUG) { - e.printStackTrace(); - } - return null; - } - } -} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/SkriptUtil.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/SkriptUtil.java new file mode 100644 index 0000000..af7f2c5 --- /dev/null +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/SkriptUtil.java @@ -0,0 +1,23 @@ +package com.sovdee.skriptparticles.util; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionSection; +import ch.njol.skript.lang.parser.ParserInstance; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class SkriptUtil { + + @SuppressWarnings({"UnstableApiUsage", "unchecked"}) + public static @Nullable T getCurrentSectionExpression(Class exprSectionClass, ParserInstance parser) { + T expression = null; + List list = parser.getCurrentSections(ExpressionSection.class); + for (ExpressionSection candidateSection : list) { + Expression candidate = candidateSection.getAsExpression(); + if (exprSectionClass.isInstance(candidate)) + expression = (T) candidate; // overwriting is good since last should be only one. + } + return expression; + } +} diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/VectorConversion.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/VectorConversion.java index 4780668..3c4d293 100644 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/VectorConversion.java +++ b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/VectorConversion.java @@ -3,8 +3,8 @@ import org.bukkit.util.Vector; import org.joml.Vector3d; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; /** * Utility class for converting between Bukkit Vectors and JOML Vector3d. @@ -12,23 +12,23 @@ public class VectorConversion { public static Vector toBukkit(Vector3d v) { - return new Vector(v.x, v.y, v.z); + return Vector.fromJOML(v); } public static Vector3d toJOML(Vector v) { - return new Vector3d(v.getX(), v.getY(), v.getZ()); + return v.toVector3d(); } - public static Set toBukkit(Set points) { - Set result = new LinkedHashSet<>(); + public static List toBukkit(List points) { + List result = new ArrayList<>(); for (Vector3d v : points) { result.add(toBukkit(v)); } return result; } - public static Set toJOML(Set points) { - Set result = new LinkedHashSet<>(); + public static List toJOML(List points) { + List result = new ArrayList<>(); for (Vector v : points) { result.add(toJOML(v)); } diff --git a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/package-info.java b/skript-particle/src/main/java/com/sovdee/skriptparticles/util/package-info.java deleted file mode 100644 index 92f5e39..0000000 --- a/skript-particle/src/main/java/com/sovdee/skriptparticles/util/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -@DefaultQualifier(NonNull.class) -package com.sovdee.skriptparticles.util; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; diff --git a/skript-particle/src/main/resources/lang/english.lang b/skript-particle/src/main/resources/lang/english.lang index 5f8d858..33e572d 100644 --- a/skript-particle/src/main/resources/lang/english.lang +++ b/skript-particle/src/main/resources/lang/english.lang @@ -2,17 +2,15 @@ version: @version@ types: customparticle: custom particle¦s - particlemotion: particle motion¦s shape: shape¦s radialshape: radial shape¦s lwhshape: length/width/height shape¦s cutoffshape: cutoff shape¦s polyshape: polygonal shape¦s shapestyle: shape style¦s + pointmodifier: point modifier¦s -particle motions: - inwards: inwards, inwards motion, towards center, towards the center - outwards: outwards, outwards motion, away from center, away from the center - clockwise: clockwise, clockwise motion - counterclockwise: anticlockwise, anticlockwise motion, counterclockwise, counterclockwise motion - none: none, no motion +wave modifier axes: + x: x, x-axis + y: y, y-axis + z: z, z-axis diff --git a/skript-particle/src/test/resources/particle-testing.sk b/skript-particle/src/test/resources/particle-testing.sk new file mode 100644 index 0000000..f9697b4 --- /dev/null +++ b/skript-particle/src/test/resources/particle-testing.sk @@ -0,0 +1,364 @@ +command /full-test: + trigger: + set {_loc} to player's location + + # --- Shape construction --- + set {_shapes::circle} to a circle with radius 1 + set {_shapes::arc} to a arc with radius 1 and angle 60 degrees + set offset vector of {_shapes::arc} to vector(3, 0, 0) + set {_shapes::cylinder} to a tube with radius 1 and height 1 + set offset vector of {_shapes::cylinder} to vector(6, 0, 0) + set {_shapes::cylarc} to a arc with radius 1, height 1, and angle 60 degrees + set offset vector of {_shapes::cylarc} to vector(9, 0, 0) + set {_shapes::helix} to a helix with radius 1, height 1, and winding rate of 1 + set offset vector of {_shapes::helix} to vector(12, 0, 0) + + set {_shapes::ellipse} to an ellipse with radius 0.5 and 1 + set offset vector of {_shapes::ellipse} to vector(0, 0, 3) + set {_shapes::elliptical-arc} to an elliptical arc with radius 0.5, 1 and angle 60 degrees + set offset vector of {_shapes::elliptical-arc} to vector(3, 0, 3) + set {_shapes::elliptical-cylinder} to an elliptical tube with radius 0.5, 1 and height 1 + set offset vector of {_shapes::elliptical-cylinder} to vector(6, 0, 3) + set {_shapes::elliptical-cylarc} to an elliptical arc with radius 0.5, 1, height 1 and angle 60 degrees + set offset vector of {_shapes::elliptical-cylarc} to vector(9, 0, 3) + + set {_shapes::sphere} to a sphere with radius 1 + set offset vector of {_shapes::sphere} to vector(0, 0, 6) + set {_shapes::cap} to a spherical cap with radius 1 and cutoff angle 60 degrees + set offset vector of {_shapes::cap} to vector(3, 0, 6) + set {_shapes::ellipsoid} to an ellipsoid with radius 1, 0.5, and 0.8 + set offset vector of {_shapes::ellipsoid} to vector(6, 0, 6) + set {_shapes::ellipsoid-2} to a hollow ellipsoid with radius 1, 0.5, and 0.8 + set offset vector of {_shapes::ellipsoid-2} to vector(9, 0, 6) + + set {_shapes::rectangle} to a rectangle with length 1 and width 1 + set offset vector of {_shapes::rectangle} to vector(0, 0, 9) + set {_shapes::cuboid} to a cuboid with length 1, width 1 and height 1 + set offset vector of {_shapes::cuboid} to vector(3, 0, 9) + + set {_shapes::line-1} to a line from vector(0, 0, 0) to vector(1, 0, 0) + set {_shapes::line-2} to a line from {_loc} to {_loc} offset by vector(0, 1, 0) + set {_shapes::line-3} to a line in direction vector(0, 0, 1) with length 1 + set offset vector of {_shapes::line-1}, {_shapes::line-2}, {_shapes::line-3} to vector(0, 0, -3) + set {_shapes::bezier} to a curve from vector(0, 0, 0) to vector(2, 0, 0) using control points vector(1, 1, 0) + set offset vector of {_shapes::bezier} to vector(6, 0, -3) + set {_shapes::heart} to a solid heart with width 2 and length 2 + set offset vector of {_shapes::heart} to vector(3, 0, -3) + set {_shapes::star} to a solid star with 5 points, inner radius 0.5, and outer radius 1 + set offset vector of {_shapes::star} to vector(9, 0, -3) + + set {_shapes::polygon-1} to a triangle with radius 1 + set offset vector of {_shapes::polygon-1} to vector(0, 0, -6) + set {_shapes::polygon-2} to a square with side length 1 + set offset vector of {_shapes::polygon-2} to vector(3, 0, -6) + set {_shapes::polygon-3} to a regular polygon with 5 sides and side length 0.6 + set offset vector of {_shapes::polygon-3} to vector(6, 0, -6) + set {_shapes::polygon-4} to a regular polygon with 12 sides and radius 1 + set offset vector of {_shapes::polygon-4} to vector(9, 0, -6) + + set {_shapes::ir-polygon} to a polygon with points vector(0, 0, 0), vector(1, 0, 0), vector(1, 1, 1) + set offset vector of {_shapes::ir-polygon} to vector(0, 0, -9) + set {_shapes::polyhedron} to an icosahedron with radius 1 + set offset vector of {_shapes::polyhedron} to vector(3, 0, -9) + + set particle of {_shapes::*} to electric spark particle with extra value 0 + show local and global axes of {_shapes::*} + + # --- Draw section body: counts per-shape callbacks --- + set {_drawCount} to 0 + loop 40 times: + draw shapes {_shapes::*} at {_loc}: + add 1 to {_drawCount} + wait 1 tick + broadcast "Drawn shapes: %({_drawCount} / 40)%" + + # --- Radius / inner+outer radius --- + loop 20 times: + add 0.1 to radius of {_shapes::*} + add 0.1 to inner radius of {_shapes::*} + add 0.1 to outer radius of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 20 times: + add -0.1 to radius of {_shapes::*} + add -0.1 to inner radius of {_shapes::*} + add -0.1 to outer radius of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Scale --- + loop 20 times: + add 0.1 to shape scale of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 20 times: + add -0.1 to shape scale of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Rotation: global axes --- + loop 40 times: + rotate shapes {_shapes::*} around vector(0, 1, 0) by 9 degrees + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 40 times: + rotate shapes {_shapes::*} around vector(1, 0, 0) by 9 degrees + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 40 times: + rotate shapes {_shapes::*} around vector(0, 0, 1) by 9 degrees + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Rotation: quaternion --- + set {_rotation} to rotation around vector(1, 1, 1) by 9 degrees + loop 40 times: + rotate shapes {_shapes::*} by {_rotation} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Rotation: local axis --- + rotate shapes {_shapes::*} around vector(1, 0, 0) by 45 degrees + loop 40 times: + rotate shapes {_shapes::*} around local z axis by 9 degrees + draw shapes {_shapes::*} at {_loc} + wait 1 tick + reset normal vector of {_shapes::*} + + # --- Cutoff angle / winding rate --- + loop 40 times: + add 7.5 to the cutoff angle of {_shapes::*} + add 0.1 to the winding rate of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 40 times: + add -7.5 to the cutoff angle of {_shapes::*} + remove 0.1 from the winding rate of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Length / side length / outer radius --- + loop 40 times: + add 0.1 to the shape length of {_shapes::*} + add 0.01 to the side length of {_shapes::*} + add 0.1 to the outer radius of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 40 times: + add -0.1 to the shape length of {_shapes::*} + add -0.01 to the side length of {_shapes::*} + add -0.1 to the outer radius of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Width / inner radius --- + loop 40 times: + add 0.1 to the shape width of {_shapes::*} + add 0.1 to the inner radius of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 40 times: + add -0.1 to the shape width of {_shapes::*} + add -0.1 to the inner radius of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Height --- + loop 40 times: + add 0.05 to the shape height of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 40 times: + add -0.05 to the shape height of {_shapes::*} + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Sides / star points --- + loop 4 times: + add 1 to the sides of {_shapes::*} + add 1 to the star points of {_shapes::*} + loop 10 times: + draw shapes {_shapes::*} at {_loc} + wait 1 tick + loop 4 times: + remove 1 from the sides of {_shapes::*} + remove 1 from the star points of {_shapes::*} + loop 10 times: + draw shapes {_shapes::*} at {_loc} + wait 1 tick + + # --- Style cycling --- + set style of {_shapes::*} to surface + loop 10 times: + draw shapes {_shapes::*} at {_loc} + wait 1 tick + set style of {_shapes::*} to filled + loop 10 times: + draw shapes {_shapes::*} at {_loc} + wait 1 tick + set style of {_shapes::*} to outline + + # --- Particle density --- + set particle count of {_shapes::*} to 50 + loop 10 times: + draw shapes {_shapes::*} at {_loc} + wait 1 tick + reset particle count of {_shapes::*} + + # --- Timed draw + animation ordering --- + set the animation order of {_shapes::*} to lowest-to-highest + draw shapes {_shapes::*} at {_loc} for 3 seconds with refresh 1 tick + wait 4 ticks + set the animation order of {_shapes::*} to highest-to-lowest + draw shapes {_shapes::*} at {_loc} for 3 seconds with refresh 1 tick + wait 4 ticks + set the animation order of {_shapes::*} to default + + hide local and global axes of {_shapes::*} + +command /magic: + trigger: + set {_shapes::outer-circle} to a circle of radius 2.225 + set {_shapes::inner-circle} to a circle of radius 2 + set {_shapes::tiny-circle} to a circle of radius 0.875 + + set {_shapes::triangle-1} to a triangle with radius 2 + set {_shapes::triangle-2} to a triangle with radius 2 + rotate shapes {_shapes::triangle-2} around y axis by 60 degrees + + set particle of {_shapes::*} to electric spark particle + + loop 200 times: + set {_view} to vector from yaw player's yaw and pitch player's pitch + set {_yaw} to yaw of {_view} + # figure out the rotation needed to rotate the shape + set {_rotation} to rotation from vector(0, 1, 0) to {_view} + draw shapes {_shapes::*} at player's eye location ~ {_view}: + # only happens for this draw call, the original shape is not modified + # note that this is called once for each shape, hence `drawn shape` and not `drawn shapes` + rotate drawn shape by {_rotation} + # the shape takes the shortest path to rotate to the desired rotation, but we don't want that here + # (we want the shape to be the same, no matter where we look) + # so we'll correct for it by rotating the shape around the y axis by the yaw of the view + rotate shape drawn shape around relative y axis by -1 * {_yaw} + + wait 1 tick + +command /star: + trigger: + set {_star} to a star with 5 points, inner radius 1, and outer radius 2 + set particle of {_star} to electric spark particle + loop 40 times: + draw shape {_star} at player + rotate shape {_star} around y axis by 9 degrees + wait 2 ticks + +command /modifier-test: + trigger: + set {_loc} to player's location + + # 1. Taper modifier on a sphere (along Y: scales XZ) + set {_sphere} to a sphere with radius 1 + set particle of {_sphere} to electric spark particle with extra value 0 + add taper from 0.0 to 1.0 along the y axis to modifiers of {_sphere} + draw shape {_sphere} at {_loc} for 5 seconds with refresh 2 ticks + wait 1 tick + + # 1b. Taper along X axis (scales YZ) + set {_sphere1b} to a sphere with radius 1 + set particle of {_sphere1b} to electric spark particle with extra value 0 + add taper from 0.0 to 1.0 along the x axis to modifiers of {_sphere1b} + draw shape {_sphere1b} at {_loc} ~ vector(0, 0, -4) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 2. Twist modifier on a cylinder along Y (rotates XZ plane) + set {_cyl} to a hollow cuboid with length 1, width 1, and height 2 + set particle of {_cyl} to electric spark particle with extra value 0 + add a twist of 180 degrees along the y axis to modifiers of {_cyl} + draw shape {_cyl} at {_loc} ~ vector(4, 0, 0) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 2b. Twist along X axis (rotates YZ plane) + set {_cyl2b} to a hollow cuboid with length 2, width 1, and height 1 + set particle of {_cyl2b} to electric spark particle with extra value 0 + add a twist of 180 degrees along the x axis to modifiers of {_cyl2b} + draw shape {_cyl2b} at {_loc} ~ vector(4, 0, -4) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 3. Wave modifier on a rectangle + set {_rect} to a solid rectangle with length 2 and width 2 + set particle of {_rect} to electric spark particle with extra value 0 + add a wave with amplitude 0.5 and frequency 4 displacing y by x to modifiers of {_rect} + add a wave with amplitude 0.5 and frequency 4 displacing y by z to modifiers of {_rect} + draw shape {_rect} at {_loc} ~ vector(8, 0, 0) for 5 seconds with refresh 2 ticks + wait 1 tick + + # stop + + # 4. Axis gradient (red→blue along Y) on a cuboid + set {_cuboid} to a cuboid with length 1, width 1 and height 2 + set particle of {_cuboid} to white dust particle + add gradient from red to blue along the y axis to modifiers of {_cuboid} + draw shape {_cuboid} at {_loc} ~ vector(0, 0, 4) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 5. Radial gradient (yellow→green) on a disc + set {_disc} to a disc with radius 1.5 + set particle of {_disc} to white dust particle + add radial gradient from yellow to green to modifiers of {_disc} + draw shape {_disc} at {_loc} ~ vector(4, 0, 4) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 6. Angular gradient (cyan→magenta) on a circle + set {_circle} to a circle with radius 1.5 + set particle of {_circle} to white dust particle + add angular gradient from cyan to magenta to modifiers of {_circle} + draw shape {_circle} at {_loc} ~ vector(8, 0, 4) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 7. Spherical gradient (white→black) on a sphere + set {_sphere2} to a solid sphere with radius 1 + set particle of {_sphere2} to white dust particle + add spherical gradient from white to black to modifiers of {_sphere2} + draw shape {_sphere2} at {_loc} ~ vector(0, 0, 8) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 8. Multi-stop gradient (red→orange→yellow→white) on a cylinder + set {_gradcyl} to a tube with radius 1 and height 2 + set particle of {_gradcyl} to white dust particle + set {_grad} to gradient from red, orange, yellow, white along the y axis + add {_grad} to modifiers of {_gradcyl} + draw shape {_gradcyl} at {_loc} ~ vector(4, 0, 8) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 9. Motion modifier (outwards) on a sphere + set {_sphere3} to a sphere with radius 1 + set particle of {_sphere3} to electric spark particle with extra value 0.3 + add motion modifier outwards to modifiers of {_sphere3} + draw shape {_sphere3} at {_loc} ~ vector(8, 0, 8) for 5 seconds with refresh 2 ticks + wait 1 tick + + # 11. Combined modifiers: taper + wave + axis gradient on one shape + set {_combined} to a tube with radius 1 and height 2 + set particle of {_combined} to white dust particle + add taper from 0.2 to 1.0 along the y axis to modifiers of {_combined} + add a wave with amplitude 0.3 and frequency 3 displacing y by t to modifiers of {_combined} + add gradient from orange to cyan along the y axis to modifiers of {_combined} + draw shape {_combined} at {_loc} ~ vector(4, 0, 12) for 5 seconds with refresh 2 ticks + + # 10. Modifier management: add, remove, delete + set {_managed} to a circle with radius 1 + set particle of {_managed} to white dust particle + set {_taper} to a taper from 0 to 1 + add {_taper} to modifiers of {_managed} + draw shape {_managed} at {_loc} ~ vector(0, 0, 12) + wait 1 second + remove {_taper} from modifiers of {_managed} + add gradient from red to blue along the y axis to modifiers of {_managed} + draw shape {_managed} at {_loc} ~ vector(0, 0, 12) + wait 1 second + delete modifiers of {_managed} + draw shape {_managed} at {_loc} ~ vector(0, 0, 12) + wait 1 second + + broadcast "Modifier test complete!"