package net.minecraft.util; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.floats.FloatArrayList; import it.unimi.dsi.fastutil.floats.FloatList; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.commons.lang3.mutable.MutableObject; public interface CubicSpline> extends ToFloatFunction { @VisibleForDebug String parityString(); CubicSpline mapAll(CubicSpline.CoordinateVisitor visitor); static > Codec> codec(Codec codec) { MutableObject>> mutableObject = new MutableObject<>(); record Point>(float location, CubicSpline value, float derivative) { } Codec> codec2 = RecordCodecBuilder.create( instance -> instance.group( Codec.FLOAT.fieldOf("location").forGetter(Point::location), Codec.lazyInitialized(mutableObject::getValue).fieldOf("value").forGetter(Point::value), Codec.FLOAT.fieldOf("derivative").forGetter(Point::derivative) ) .apply(instance, (f, cubicSpline, g) -> new Point(f, cubicSpline, g)) ); Codec> codec3 = RecordCodecBuilder.create( instance -> instance.group( codec.fieldOf("coordinate").forGetter(CubicSpline.Multipoint::coordinate), ExtraCodecs.nonEmptyList(codec2.listOf()) .fieldOf("points") .forGetter( multipoint -> IntStream.range(0, multipoint.locations.length) .mapToObj(i -> new Point(multipoint.locations()[i], (CubicSpline)multipoint.values().get(i), multipoint.derivatives()[i])) .toList() ) ) .apply(instance, (toFloatFunction, list) -> { float[] fs = new float[list.size()]; ImmutableList.Builder> builder = ImmutableList.builder(); float[] gs = new float[list.size()]; for (int i = 0; i < list.size(); i++) { Point lv = (Point)list.get(i); fs[i] = lv.location(); builder.add(lv.value()); gs[i] = lv.derivative(); } return CubicSpline.Multipoint.create((I)toFloatFunction, fs, builder.build(), gs); }) ); mutableObject.setValue( Codec.either(Codec.FLOAT, codec3) .xmap( either -> either.map(CubicSpline.Constant::new, multipoint -> multipoint), cubicSpline -> cubicSpline instanceof CubicSpline.Constant constant ? Either.left(constant.value()) : Either.right((CubicSpline.Multipoint)cubicSpline) ) ); return mutableObject.getValue(); } static > CubicSpline constant(float value) { return new CubicSpline.Constant<>(value); } static > CubicSpline.Builder builder(I coordinate) { return new CubicSpline.Builder<>(coordinate); } static > CubicSpline.Builder builder(I coordinate, ToFloatFunction valueTransformer) { return new CubicSpline.Builder<>(coordinate, valueTransformer); } public static final class Builder> { private final I coordinate; private final ToFloatFunction valueTransformer; private final FloatList locations = new FloatArrayList(); private final List> values = Lists.>newArrayList(); private final FloatList derivatives = new FloatArrayList(); protected Builder(I coordinate) { this(coordinate, ToFloatFunction.IDENTITY); } protected Builder(I coordinate, ToFloatFunction valueTransformer) { this.coordinate = coordinate; this.valueTransformer = valueTransformer; } public CubicSpline.Builder addPoint(float location, float value) { return this.addPoint(location, new CubicSpline.Constant<>(this.valueTransformer.apply(value)), 0.0F); } public CubicSpline.Builder addPoint(float location, float value, float derivative) { return this.addPoint(location, new CubicSpline.Constant<>(this.valueTransformer.apply(value)), derivative); } public CubicSpline.Builder addPoint(float location, CubicSpline value) { return this.addPoint(location, value, 0.0F); } private CubicSpline.Builder addPoint(float location, CubicSpline value, float derivative) { if (!this.locations.isEmpty() && location <= this.locations.getFloat(this.locations.size() - 1)) { throw new IllegalArgumentException("Please register points in ascending order"); } else { this.locations.add(location); this.values.add(value); this.derivatives.add(derivative); return this; } } public CubicSpline build() { if (this.locations.isEmpty()) { throw new IllegalStateException("No elements added"); } else { return CubicSpline.Multipoint.create(this.coordinate, this.locations.toFloatArray(), ImmutableList.copyOf(this.values), this.derivatives.toFloatArray()); } } } @VisibleForDebug public record Constant>(float value) implements CubicSpline { @Override public float apply(C object) { return this.value; } @Override public String parityString() { return String.format(Locale.ROOT, "k=%.3f", this.value); } @Override public float minValue() { return this.value; } @Override public float maxValue() { return this.value; } @Override public CubicSpline mapAll(CubicSpline.CoordinateVisitor visitor) { return this; } } public interface CoordinateVisitor { I visit(I object); } @VisibleForDebug public record Multipoint>( I coordinate, float[] locations, List> values, float[] derivatives, float minValue, float maxValue ) implements CubicSpline { public Multipoint(I coordinate, float[] locations, List> values, float[] derivatives, float minValue, float maxValue) { validateSizes(locations, values, derivatives); this.coordinate = coordinate; this.locations = locations; this.values = values; this.derivatives = derivatives; this.minValue = minValue; this.maxValue = maxValue; } static > CubicSpline.Multipoint create( I coordinate, float[] locations, List> values, float[] derivatives ) { validateSizes(locations, values, derivatives); int i = locations.length - 1; float f = Float.POSITIVE_INFINITY; float g = Float.NEGATIVE_INFINITY; float h = coordinate.minValue(); float j = coordinate.maxValue(); if (h < locations[0]) { float k = linearExtend(h, locations, ((CubicSpline)values.get(0)).minValue(), derivatives, 0); float l = linearExtend(h, locations, ((CubicSpline)values.get(0)).maxValue(), derivatives, 0); f = Math.min(f, Math.min(k, l)); g = Math.max(g, Math.max(k, l)); } if (j > locations[i]) { float k = linearExtend(j, locations, ((CubicSpline)values.get(i)).minValue(), derivatives, i); float l = linearExtend(j, locations, ((CubicSpline)values.get(i)).maxValue(), derivatives, i); f = Math.min(f, Math.min(k, l)); g = Math.max(g, Math.max(k, l)); } for (CubicSpline cubicSpline : values) { f = Math.min(f, cubicSpline.minValue()); g = Math.max(g, cubicSpline.maxValue()); } for (int m = 0; m < i; m++) { float l = locations[m]; float n = locations[m + 1]; float o = n - l; CubicSpline cubicSpline2 = (CubicSpline)values.get(m); CubicSpline cubicSpline3 = (CubicSpline)values.get(m + 1); float p = cubicSpline2.minValue(); float q = cubicSpline2.maxValue(); float r = cubicSpline3.minValue(); float s = cubicSpline3.maxValue(); float t = derivatives[m]; float u = derivatives[m + 1]; if (t != 0.0F || u != 0.0F) { float v = t * o; float w = u * o; float x = Math.min(p, r); float y = Math.max(q, s); float z = v - s + p; float aa = v - r + q; float ab = -w + r - q; float ac = -w + s - p; float ad = Math.min(z, ab); float ae = Math.max(aa, ac); f = Math.min(f, x + 0.25F * ad); g = Math.max(g, y + 0.25F * ae); } } return new CubicSpline.Multipoint<>(coordinate, locations, values, derivatives, f, g); } private static float linearExtend(float coordinate, float[] locations, float value, float[] derivatives, int index) { float f = derivatives[index]; return f == 0.0F ? value : value + f * (coordinate - locations[index]); } private static > void validateSizes(float[] locations, List> values, float[] derivatives) { if (locations.length != values.size() || locations.length != derivatives.length) { throw new IllegalArgumentException("All lengths must be equal, got: " + locations.length + " " + values.size() + " " + derivatives.length); } else if (locations.length == 0) { throw new IllegalArgumentException("Cannot create a multipoint spline with no points"); } } @Override public float apply(C object) { float f = this.coordinate.apply(object); int i = findIntervalStart(this.locations, f); int j = this.locations.length - 1; if (i < 0) { return linearExtend(f, this.locations, ((CubicSpline)this.values.get(0)).apply(object), this.derivatives, 0); } else if (i == j) { return linearExtend(f, this.locations, ((CubicSpline)this.values.get(j)).apply(object), this.derivatives, j); } else { float g = this.locations[i]; float h = this.locations[i + 1]; float k = (f - g) / (h - g); ToFloatFunction toFloatFunction = (ToFloatFunction)this.values.get(i); ToFloatFunction toFloatFunction2 = (ToFloatFunction)this.values.get(i + 1); float l = this.derivatives[i]; float m = this.derivatives[i + 1]; float n = toFloatFunction.apply(object); float o = toFloatFunction2.apply(object); float p = l * (h - g) - (o - n); float q = -m * (h - g) + (o - n); return Mth.lerp(k, n, o) + k * (1.0F - k) * Mth.lerp(k, p, q); } } private static int findIntervalStart(float[] locations, float start) { return Mth.binarySearch(0, locations.length, i -> start < locations[i]) - 1; } @VisibleForTesting @Override public String parityString() { return "Spline{coordinate=" + this.coordinate + ", locations=" + this.toString(this.locations) + ", derivatives=" + this.toString(this.derivatives) + ", values=" + (String)this.values.stream().map(CubicSpline::parityString).collect(Collectors.joining(", ", "[", "]")) + "}"; } private String toString(float[] locations) { return "[" + (String)IntStream.range(0, locations.length) .mapToDouble(i -> locations[i]) .mapToObj(d -> String.format(Locale.ROOT, "%.3f", d)) .collect(Collectors.joining(", ")) + "]"; } @Override public CubicSpline mapAll(CubicSpline.CoordinateVisitor visitor) { return create( visitor.visit(this.coordinate), this.locations, this.values().stream().map(cubicSpline -> cubicSpline.mapAll(visitor)).toList(), this.derivatives ); } } }