chore(history): squash 5 commit(s) from 2026-09-07

- refactor: Phase 8 pass 2 — Renderer2D split + Triangulator decomp
- test: add TriangulatorSolverTest (11 tests) - solve null/empty/single/two throws - perpendicular intersection - distanceToLine - formatPercent, formatThrowPoint, formatCoordinateLine (overworld/nether) - formatCopyCoordinates
- fix: Triangulator - replace static Minecraft field with lazy access
- fix: Coordinates HUD - replace static Minecraft field with lazy access
- fix: replace static Minecraft fields with lazy access in HUD elements (part 2)
This commit is contained in:
loki5512344 2026-09-07 14:12:53 +02:00
parent d83f9a29ff
commit e1d7d19a83
48 changed files with 2915 additions and 2681 deletions

View file

@ -6,6 +6,9 @@
- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга - **KISS / DRY / SOLID** — простая структура, без оверинжиниринга
- **Макс. 200 строк на файл** (было >1500) — превышение = разбить на модули - **Макс. 200 строк на файл** (было >1500) — превышение = разбить на модули
- **Макс. 4 файла на папку** — больше = новая подпапка по смыслу - **Макс. 4 файла на папку** — больше = новая подпапка по смыслу
- **Публичный API можно менять свободно** (решение владельца 2026-09-05):
KISS важнее обратной совместимости; при рефакторинге разрешено менять
сигнатуры, удалять делегирование и мёртвый код, обновляя вызывающих
- **Никакого статика** — `Minecraft.getInstance()` только лениво, никогда в поле - **Никакого статика** — `Minecraft.getInstance()` только лениво, никогда в поле
- **Никаких this-escape** — конструктор не публикует `this` - **Никаких this-escape** — конструктор не публикует `this`
- **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном cancel - **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном cancel

View file

@ -62,7 +62,6 @@ public final class Coordinates extends DraggableHudElement implements Scriptable
private static final String EFFECT_BLUR = "Blur"; private static final String EFFECT_BLUR = "Blur";
private static final String EFFECT_GLASS = "Glass"; private static final String EFFECT_GLASS = "Glass";
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final EnumValue<NetherMode> netherMode = private final EnumValue<NetherMode> netherMode =
enumSetting("xyz_nether", NetherMode.ON, NetherMode.OFF, NetherMode.ON, NetherMode.ONLY_NETHER); enumSetting("xyz_nether", NetherMode.ON, NetherMode.OFF, NetherMode.ON, NetherMode.ONLY_NETHER);
@ -124,6 +123,7 @@ public final class Coordinates extends DraggableHudElement implements Scriptable
int screenW, int screenW,
int screenH) { int screenH) {
boolean preview = DraggableHudElementRegistry.isForceVisible(); boolean preview = DraggableHudElementRegistry.isForceVisible();
Minecraft mc = Minecraft.getInstance();
if (mc == null || (mc.player == null && !preview)) { if (mc == null || (mc.player == null && !preview)) {
width = 0f; width = 0f;
height = 0f; height = 0f;
@ -137,10 +137,10 @@ public final class Coordinates extends DraggableHudElement implements Scriptable
return; return;
} }
boolean inNether = mc != null && mc.level != null && mc.level.dimension() == Level.NETHER; boolean inNether = mc.level != null && mc.level.dimension() == Level.NETHER;
double rawX = mc != null && mc.player != null ? mc.player.getX() : 0.0; double rawX = mc.player != null ? mc.player.getX() : 0.0;
double rawY = mc != null && mc.player != null ? mc.player.getY() : 0.0; double rawY = mc.player != null ? mc.player.getY() : 0.0;
double rawZ = mc != null && mc.player != null ? mc.player.getZ() : 0.0; double rawZ = mc.player != null ? mc.player.getZ() : 0.0;
int px = Mth.floor(rawX); int px = Mth.floor(rawX);
int py = Mth.floor(rawY); int py = Mth.floor(rawY);
int pz = Mth.floor(rawZ); int pz = Mth.floor(rawZ);

View file

@ -61,7 +61,6 @@ public final class Fps extends DraggableHudElement implements ScriptableHudStatW
private static final long DIGIT_ANIMATION_DURATION_MS = 200L; private static final long DIGIT_ANIMATION_DURATION_MS = 200L;
private static final float DIGIT_ANIMATION_OFFSET = 8.0f; private static final float DIGIT_ANIMATION_OFFSET = 8.0f;
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final NumberValue<Double> scale = num("fps_scale", 2.37, 0.5, 5.0); private final NumberValue<Double> scale = num("fps_scale", 2.37, 0.5, 5.0);
private final ModeValue colorMode = mode("fps_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); private final ModeValue colorMode = mode("fps_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM});
@ -129,7 +128,7 @@ public final class Fps extends DraggableHudElement implements ScriptableHudStatW
int screenW, int screenW,
int screenH) { int screenH) {
boolean preview = DraggableHudElementRegistry.isForceVisible(); boolean preview = DraggableHudElementRegistry.isForceVisible();
if (mc == null && !preview) { if (Minecraft.getInstance() == null && !preview) {
width = 0f; width = 0f;
height = 0f; height = 0f;
scriptModel.setVisible(false); scriptModel.setVisible(false);

View file

@ -74,7 +74,6 @@ public final class GameTime extends DraggableHudElement implements ScriptableHud
private static final int ICON_THUNDER_SNOW_DAY = 0xEA02; private static final int ICON_THUNDER_SNOW_DAY = 0xEA02;
private static final int ICON_THUNDER_SNOW_NIGHT = 0xEA0A; private static final int ICON_THUNDER_SNOW_NIGHT = 0xEA0A;
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final BooleanValue showWeather = bool("game_time_weather", "weather", true); private final BooleanValue showWeather = bool("game_time_weather", "weather", true);
private final NumberValue<Double> scale = num("game_time_scale", 2.37, 0.5, 5.0); private final NumberValue<Double> scale = num("game_time_scale", 2.37, 0.5, 5.0);
@ -143,6 +142,7 @@ public final class GameTime extends DraggableHudElement implements ScriptableHud
int screenW, int screenW,
int screenH) { int screenH) {
boolean preview = DraggableHudElementRegistry.isForceVisible(); boolean preview = DraggableHudElementRegistry.isForceVisible();
Minecraft mc = Minecraft.getInstance();
if ((mc == null || mc.level == null) && !preview) { if ((mc == null || mc.level == null) && !preview) {
width = 0f; width = 0f;
height = 0f; height = 0f;
@ -235,6 +235,7 @@ public final class GameTime extends DraggableHudElement implements ScriptableHud
private int resolveIcon(long time) { private int resolveIcon(long time) {
if (!showWeather.get()) return resolveClearIcon(time); if (!showWeather.get()) return resolveClearIcon(time);
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.level == null || mc.player == null) return resolveClearIcon(time); if (mc == null || mc.level == null || mc.player == null) return resolveClearIcon(time);
boolean thunder = WorldTweaks.isServerThundering(mc.level); boolean thunder = WorldTweaks.isServerThundering(mc.level);
@ -258,6 +259,7 @@ public final class GameTime extends DraggableHudElement implements ScriptableHud
} }
private boolean isSnowingAtPlayer() { private boolean isSnowingAtPlayer() {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.level == null || mc.player == null) return false; if (mc == null || mc.level == null || mc.player == null) return false;
BlockPos pos = mc.player.blockPosition(); BlockPos pos = mc.player.blockPosition();
Biome.Precipitation precipitation = mc.level.getBiome(pos).value() Biome.Precipitation precipitation = mc.level.getBiome(pos).value()

View file

@ -57,7 +57,6 @@ public final class Keybinds extends DraggableHudElement {
private static final String MODE_PRESS = "Press"; private static final String MODE_PRESS = "Press";
private static final String MODE_HOLD = "Hold"; private static final String MODE_HOLD = "Hold";
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel();
private final NumberValue<Double> scaleValue = private final NumberValue<Double> scaleValue =
@ -237,6 +236,7 @@ public final class Keybinds extends DraggableHudElement {
int screenW, int screenW,
int screenH) { int screenH) {
boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); boolean forceVisible = DraggableHudElementRegistry.isForceVisible();
Minecraft mc = Minecraft.getInstance();
if (mc == null || (mc.player == null && !forceVisible)) { if (mc == null || (mc.player == null && !forceVisible)) {
width = 0.0f; width = 0.0f;
height = 0.0f; height = 0.0f;

View file

@ -59,7 +59,6 @@ public final class Memory extends DraggableHudElement implements ScriptableHudSt
private static final String EFFECT_BLUR = "Blur"; private static final String EFFECT_BLUR = "Blur";
private static final String EFFECT_GLASS = "Glass"; private static final String EFFECT_GLASS = "Glass";
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final NumberValue<Double> scale = num("memory_scale", 2.37, 0.5, 5.0); private final NumberValue<Double> scale = num("memory_scale", 2.37, 0.5, 5.0);
private final ModeValue colorMode = mode("memory_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); private final ModeValue colorMode = mode("memory_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM});
@ -127,7 +126,7 @@ public final class Memory extends DraggableHudElement implements ScriptableHudSt
int screenW, int screenW,
int screenH) { int screenH) {
boolean preview = DraggableHudElementRegistry.isForceVisible(); boolean preview = DraggableHudElementRegistry.isForceVisible();
if (mc == null && !preview) { if (Minecraft.getInstance() == null && !preview) {
width = 0f; width = 0f;
height = 0f; height = 0f;
scriptModel.setVisible(false); scriptModel.setVisible(false);

View file

@ -46,7 +46,6 @@ public final class ModuleList extends DraggableHudElement implements ConfigValue
private static final float BASE_RECT_EXTRA = 1f; private static final float BASE_RECT_EXTRA = 1f;
private static final float BASE_RADIUS = 2f; private static final float BASE_RADIUS = 2f;
private static final float BASE_SOFTNESS = 0.55f; private static final float BASE_SOFTNESS = 0.55f;
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final NumberValue<Double> scaleValue = private final NumberValue<Double> scaleValue =
new NumberValue<>("module_list_scale", 3.67, 0.5, 5.0); new NumberValue<>("module_list_scale", 3.67, 0.5, 5.0);
@ -140,6 +139,7 @@ public final class ModuleList extends DraggableHudElement implements ConfigValue
float tickDelta, float tickDelta,
int screenW, int screenW,
int screenH) { int screenH) {
Minecraft mc = Minecraft.getInstance();
if (!isEnabled() || mc == null || mc.player == null) { if (!isEnabled() || mc == null || mc.player == null) {
rows.clear(); rows.clear();
width = 0f; width = 0f;

View file

@ -60,7 +60,6 @@ public final class Ping extends DraggableHudElement implements ScriptableHudStat
private static final String EFFECT_BLUR = "Blur"; private static final String EFFECT_BLUR = "Blur";
private static final String EFFECT_GLASS = "Glass"; private static final String EFFECT_GLASS = "Glass";
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final NumberValue<Double> scale = num("ping_scale", 2.58, 0.5, 5.0); private final NumberValue<Double> scale = num("ping_scale", 2.58, 0.5, 5.0);
private final ModeValue colorMode = mode("ping_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); private final ModeValue colorMode = mode("ping_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM});
@ -118,7 +117,7 @@ public final class Ping extends DraggableHudElement implements ScriptableHudStat
int screenW, int screenW,
int screenH) { int screenH) {
boolean preview = DraggableHudElementRegistry.isForceVisible(); boolean preview = DraggableHudElementRegistry.isForceVisible();
if (mc == null && !preview) { if (Minecraft.getInstance() == null && !preview) {
width = 0f; width = 0f;
height = 0f; height = 0f;
scriptModel.setVisible(false); scriptModel.setVisible(false);
@ -131,7 +130,7 @@ public final class Ping extends DraggableHudElement implements ScriptableHudStat
return; return;
} }
int ping = NetworkStatsUtil.getPing(mc); int ping = NetworkStatsUtil.getPing(Minecraft.getInstance());
String value = getPingValue(ping); String value = getPingValue(ping);
String unit = "ms"; String unit = "ms";
updatePalette(); updatePalette();

View file

@ -85,7 +85,6 @@ public final class Potions extends DraggableHudElement {
Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/night_vision.png") Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/night_vision.png")
}; };
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel();
private final NumberValue<Double> scaleValue = private final NumberValue<Double> scaleValue =
@ -182,6 +181,7 @@ public final class Potions extends DraggableHudElement {
int screenW, int screenW,
int screenH) { int screenH) {
boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); boolean forceVisible = DraggableHudElementRegistry.isForceVisible();
Minecraft mc = Minecraft.getInstance();
if (mc == null || (mc.player == null && !forceVisible)) { if (mc == null || (mc.player == null && !forceVisible)) {
width = 0.0f; width = 0.0f;
height = 0.0f; height = 0.0f;
@ -434,6 +434,7 @@ public final class Potions extends DraggableHudElement {
private List<Row> collectRows() { private List<Row> collectRows() {
List<Row> rows = new ArrayList<>(); List<Row> rows = new ArrayList<>();
Minecraft mc = Minecraft.getInstance();
if (mc != null && mc.player != null) { if (mc != null && mc.player != null) {
mc.player.getActiveEffects().stream() mc.player.getActiveEffects().stream()
.sorted((a, b) -> String.CASE_INSENSITIVE_ORDER.compare( .sorted((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(

View file

@ -24,7 +24,7 @@ import net.minecraft.resources.Identifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
final class ScriptedTriangulatorHudPanel { public final class ScriptedTriangulatorHudPanel {
private static final String LAYOUT_ID = "lovisual:api/hud/draggable/triangulator"; private static final String LAYOUT_ID = "lovisual:api/hud/draggable/triangulator";
private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID);
private final CachedUiScriptRuntime runtime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); private final CachedUiScriptRuntime runtime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter());
@ -33,7 +33,7 @@ final class ScriptedTriangulatorHudPanel {
return String.format("#%08X", argb); return String.format("#%08X", argb);
} }
static LinkedHashMap<String, Object> row(String key, public static LinkedHashMap<String, Object> row(String key,
String label, String label,
String value, String value,
int labelColor, int labelColor,
@ -53,15 +53,15 @@ final class ScriptedTriangulatorHudPanel {
return row; return row;
} }
static String idString(Identifier id) { public static String idString(Identifier id) {
return id != null ? id.toString() : ""; return id != null ? id.toString() : "";
} }
static List<LinkedHashMap<String, Object>> rows() { public static List<LinkedHashMap<String, Object>> rows() {
return new ArrayList<>(); return new ArrayList<>();
} }
boolean render(Renderer2D renderer, public boolean render(Renderer2D renderer,
TextRenderer textRenderer, TextRenderer textRenderer,
GuiGraphicsExtractor ctx, GuiGraphicsExtractor ctx,
float tickDelta, float tickDelta,
@ -130,7 +130,7 @@ final class ScriptedTriangulatorHudPanel {
} }
patch.put(prop, value); patch.put(prop, value);
} }
record Palette(int headerLeft, public record Palette(int headerLeft,
int headerRight, int headerRight,
int bodyLeft, int bodyLeft,
int bodyRight, int bodyRight,
@ -164,7 +164,7 @@ final class ScriptedTriangulatorHudPanel {
} }
} }
record Panel(Palette palette, public record Panel(Palette palette,
float x, float x,
float y, float y,
float width, float width,
@ -191,7 +191,7 @@ final class ScriptedTriangulatorHudPanel {
String confidenceText, String confidenceText,
int statusColor, int statusColor,
List<LinkedHashMap<String, Object>> rows) { List<LinkedHashMap<String, Object>> rows) {
Panel(Palette palette, public Panel(Palette palette,
float x, float x,
float y, float y,
float width, float width,

View file

@ -73,7 +73,6 @@ public final class SystemTime extends DraggableHudElement implements ScriptableH
iconString(0xEA13) iconString(0xEA13)
}; };
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final BooleanValue showSeconds = bool("system_time_seconds", false); private final BooleanValue showSeconds = bool("system_time_seconds", false);
private final NumberValue<Double> scale = num("system_time_scale", 2.37, 0.5, 5.0); private final NumberValue<Double> scale = num("system_time_scale", 2.37, 0.5, 5.0);
@ -143,7 +142,7 @@ public final class SystemTime extends DraggableHudElement implements ScriptableH
int screenW, int screenW,
int screenH) { int screenH) {
boolean preview = DraggableHudElementRegistry.isForceVisible(); boolean preview = DraggableHudElementRegistry.isForceVisible();
if (mc == null && !preview) { if (Minecraft.getInstance() == null && !preview) {
width = 0f; width = 0f;
height = 0f; height = 0f;
scriptModel.setVisible(false); scriptModel.setVisible(false);

View file

@ -60,7 +60,6 @@ public final class Tps extends DraggableHudElement implements ScriptableHudStatW
private static final String EFFECT_BLUR = "Blur"; private static final String EFFECT_BLUR = "Blur";
private static final String EFFECT_GLASS = "Glass"; private static final String EFFECT_GLASS = "Glass";
private final Minecraft mc = Minecraft.getInstance();
private final HudGlobalConfig hud = HudGlobalConfig.get(); private final HudGlobalConfig hud = HudGlobalConfig.get();
private final BooleanValue extra = bool("tps_extra", false); private final BooleanValue extra = bool("tps_extra", false);
private final NumberValue<Double> scale = num("tps_scale", 2.37, 0.5, 5.0); private final NumberValue<Double> scale = num("tps_scale", 2.37, 0.5, 5.0);
@ -149,7 +148,7 @@ public final class Tps extends DraggableHudElement implements ScriptableHudStatW
int screenW, int screenW,
int screenH) { int screenH) {
boolean preview = DraggableHudElementRegistry.isForceVisible(); boolean preview = DraggableHudElementRegistry.isForceVisible();
if (mc == null && !preview) { if (Minecraft.getInstance() == null && !preview) {
width = 0f; width = 0f;
height = 0f; height = 0f;
scriptModel.setVisible(false); scriptModel.setVisible(false);
@ -162,6 +161,7 @@ public final class Tps extends DraggableHudElement implements ScriptableHudStatW
return; return;
} }
Minecraft mc = Minecraft.getInstance();
float tps = NetworkStatsUtil.getTps(mc); float tps = NetworkStatsUtil.getTps(mc);
boolean showExtra = extra.get(); boolean showExtra = extra.get();
float clientTps = showExtra ? NetworkStatsUtil.getClientTps(mc) : 0f; float clientTps = showExtra ? NetworkStatsUtil.getClientTps(mc) : 0f;

View file

@ -0,0 +1,152 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.projectile.EyeOfEnder;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.List;
/**
* Rolling observation window for one thrown eye of ender plus the
* quality-scored capture computed once its trajectory stabilizes.
*/
public final class TrackedEye {
private static final double MIN_HORIZONTAL_SPEED = 0.03;
private static final int MAX_OBSERVATIONS = 14;
private final List<Observation> observations = new ArrayList<>();
private long lastSeenMs;
private boolean captured;
TrackedEye(int entityId) {
}
public void observe(EyeOfEnder eye, long now) {
if (eye == null) {
return;
}
Vec3 velocity = eye.getDeltaMovement();
double speed = Math.sqrt(velocity.x * velocity.x + velocity.z * velocity.z);
if (speed < MIN_HORIZONTAL_SPEED) {
lastSeenMs = now;
return;
}
observations.add(new Observation(eye.getX(), eye.getZ(), velocity.x / speed, velocity.z / speed, speed));
if (observations.size() > MAX_OBSERVATIONS) {
observations.remove(0);
}
lastSeenMs = now;
}
public TrackedCapture tryCapture(int minTicks, double stabilityThreshold, double deviationLimit) {
if (captured || observations.size() < Math.max(2, minTicks)) {
return null;
}
double dirX = 0.0;
double dirZ = 0.0;
double avgSpeed = 0.0;
for (Observation observation : observations) {
dirX += observation.dirX();
dirZ += observation.dirZ();
avgSpeed += observation.speed();
}
avgSpeed /= observations.size();
double dirLen = Math.sqrt(dirX * dirX + dirZ * dirZ);
if (dirLen <= 1.0e-6) {
return null;
}
dirX /= dirLen;
dirZ /= dirLen;
double centroidX = 0.0;
double centroidZ = 0.0;
double avgCos = 0.0;
double minCos = 1.0;
for (Observation observation : observations) {
centroidX += observation.x();
centroidZ += observation.z();
double dot = Mth.clamp(observation.dirX() * dirX + observation.dirZ() * dirZ, -1.0, 1.0);
avgCos += dot;
minCos = Math.min(minCos, dot);
}
centroidX /= observations.size();
centroidZ /= observations.size();
avgCos /= observations.size();
double normalX = -dirZ;
double normalZ = dirX;
double deviation = 0.0;
for (Observation observation : observations) {
deviation += Math.abs((observation.x() - centroidX) * normalX + (observation.z() - centroidZ) * normalZ);
}
deviation /= observations.size();
if (avgCos < stabilityThreshold || deviation > deviationLimit) {
return null;
}
double stabilityScore = Mth.clamp(
(avgCos - stabilityThreshold) / Math.max(1.0e-5, 1.0 - stabilityThreshold),
0.0,
1.0
);
double minCosScore = Mth.clamp(
(minCos - (stabilityThreshold - 0.01)) / Math.max(1.0e-5, 1.01 - stabilityThreshold),
0.0,
1.0
);
double deviationScore = Math.exp(-deviation / 0.15);
double speedScore = Mth.clamp(avgSpeed / 0.35, 0.0, 1.0);
double quality = Mth.clamp(
stabilityScore * 0.40 + minCosScore * 0.25 + deviationScore * 0.20 + speedScore * 0.15,
0.0,
1.0
);
return new TrackedCapture(
centroidX,
centroidZ,
dirX,
dirZ,
quality,
avgCos,
deviation,
observations.size()
);
}
public boolean captured() {
return captured;
}
public void markCaptured() {
captured = true;
}
public long lastSeenMs() {
return lastSeenMs;
}
public record Observation(double x, double z, double dirX, double dirZ, double speed) {
}
public record TrackedCapture(double pointX,
double pointZ,
double dirX,
double dirZ,
double quality,
double stability,
double deviation,
int samples) {
}
}

View file

@ -0,0 +1,193 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator;
import net.minecraft.util.Mth;
import java.util.List;
import java.util.Locale;
/**
* Pure triangulation math and formatting: weighted pairwise line
* intersections of cached throws plus the user-facing status strings.
*/
public final class TriangulatorSolver {
private static final double MIN_SOLVABLE_ANGLE_DEG = 0.05;
private static final int COPY_COORD_Y = 64;
public static final List<CachedThrow> PREVIEW_THROWS = List.of(
new CachedThrow(1L, 110.0, 58.0, 0.9238795, -0.3826834, 0.91, 0.996, 0.08, 8, 0L),
new CachedThrow(2L, 188.0, -24.0, 0.7071067, -0.7071067, 0.87, 0.992, 0.12, 7, 0L),
new CachedThrow(3L, 254.0, -102.0, 0.4713967, -0.8819212, 0.82, 0.989, 0.19, 6, 0L)
);
private TriangulatorSolver() {
}
public static SolveResult previewResult() {
return solve(PREVIEW_THROWS, 6.0);
}
public static SolveResult solve(List<CachedThrow> throwsToSolve, double minAngleThreshold) {
if (throwsToSolve == null || throwsToSolve.isEmpty()) {
return null;
}
if (throwsToSolve.size() == 1) {
CachedThrow only = throwsToSolve.get(0);
return new SolveResult(
false,
false,
only.pointX(),
only.pointZ(),
0.0,
0.0,
only.quality(),
Mth.floor(only.pointX() / 16.0),
Mth.floor(only.pointZ() / 16.0)
);
}
double qualitySum = 0.0;
double strongestAngle = 0.0;
double pairX = 0.0;
double pairZ = 0.0;
double pairWeightSum = 0.0;
double crossThreshold = Math.sin(Math.toRadians(MIN_SOLVABLE_ANGLE_DEG));
for (int i = 0; i < throwsToSolve.size(); i++) {
CachedThrow throwI = throwsToSolve.get(i);
qualitySum += throwI.quality();
for (int j = i + 1; j < throwsToSolve.size(); j++) {
CachedThrow throwJ = throwsToSolve.get(j);
double dot = Math.abs(throwI.dirX() * throwJ.dirX() + throwI.dirZ() * throwJ.dirZ());
double angle = Math.toDegrees(Math.acos(Mth.clamp(dot, -1.0, 1.0)));
strongestAngle = Math.max(strongestAngle, angle);
double cross = throwI.dirX() * throwJ.dirZ() - throwI.dirZ() * throwJ.dirX();
if (Math.abs(cross) <= crossThreshold) {
continue;
}
double deltaX = throwJ.pointX() - throwI.pointX();
double deltaZ = throwJ.pointZ() - throwI.pointZ();
double t = (deltaX * throwJ.dirZ() - deltaZ * throwJ.dirX()) / cross;
double intersectionX = throwI.pointX() + throwI.dirX() * t;
double intersectionZ = throwI.pointZ() + throwI.dirZ() * t;
double pairWeight = (0.35 + throwI.quality() * 0.65)
* (0.35 + throwJ.quality() * 0.65)
* Mth.clamp(0.35 + angle / Math.max(0.35, minAngleThreshold), 0.35, 4.0);
pairX += intersectionX * pairWeight;
pairZ += intersectionZ * pairWeight;
pairWeightSum += pairWeight;
}
}
if (pairWeightSum <= 1.0e-8) {
return new SolveResult(false, false, 0.0, 0.0, Double.POSITIVE_INFINITY, strongestAngle, 0.0, 0, 0);
}
double x = pairX / pairWeightSum;
double z = pairZ / pairWeightSum;
double errorSum = 0.0;
for (CachedThrow cachedThrow : throwsToSolve) {
double d = distanceToLine(x, z, cachedThrow);
errorSum += d * d;
}
double rmse = Math.sqrt(errorSum / throwsToSolve.size());
double quality = qualitySum / throwsToSolve.size();
double effectiveAngleThreshold = Math.max(0.35, Math.min(minAngleThreshold, 2.0));
double angleScore = Mth.clamp(
(strongestAngle - effectiveAngleThreshold) / Math.max(0.5, 12.0 - effectiveAngleThreshold),
0.0,
1.0
);
double errorScore = Mth.clamp(1.0 - rmse / 64.0, 0.0, 1.0);
double countBonus = Mth.clamp((throwsToSolve.size() - 1.0) / 3.0, 0.0, 1.0);
double confidence = Mth.clamp(
quality * 0.45 + angleScore * 0.25 + errorScore * 0.20 + countBonus * 0.10,
0.0,
1.0
);
boolean ready = strongestAngle >= effectiveAngleThreshold && rmse <= 96.0 && confidence >= 0.55;
return new SolveResult(
true,
ready,
x,
z,
rmse,
strongestAngle,
confidence,
Mth.floor(x / 16.0),
Mth.floor(z / 16.0)
);
}
public static double distanceToLine(double x, double z, CachedThrow cachedThrow) {
double nx = -cachedThrow.dirZ();
double nz = cachedThrow.dirX();
return Math.abs((x - cachedThrow.pointX()) * nx + (z - cachedThrow.pointZ()) * nz);
}
public static String formatPercent(double value) {
return Math.round(Mth.clamp(value, 0.0, 1.0) * 100.0) + "%";
}
public static String formatThrowPoint(CachedThrow cachedThrow) {
return String.format(Locale.ROOT, "%d %d",
Math.round(cachedThrow.pointX()),
Math.round(cachedThrow.pointZ()));
}
public static String formatCopyCoordinates(SolveResult result) {
if (result == null || !result.hasSolution()) {
return "";
}
return String.format(Locale.ROOT, "%d %d %d", Math.round(result.x()), COPY_COORD_Y, Math.round(result.z()));
}
public static String formatCoordinateLine(double overworldX, double overworldZ, boolean nether) {
if (nether) {
return String.format(Locale.ROOT, "N %d %d", Math.round(overworldX / 8.0), Math.round(overworldZ / 8.0));
}
return String.format(Locale.ROOT, "X %d Z %d", Math.round(overworldX), Math.round(overworldZ));
}
public static String formatResultLine(double overworldX, double overworldZ, double rmse, boolean nether) {
if (nether) {
return String.format(Locale.ROOT, "OW %d %d Err %.1fb",
Math.round(overworldX), Math.round(overworldZ), rmse);
}
return String.format(Locale.ROOT, "Err %.1fb Chunk %d %d",
rmse, Mth.floor(overworldX / 16.0), Mth.floor(overworldZ / 16.0));
}
public record CachedThrow(long serial,
double pointX,
double pointZ,
double dirX,
double dirZ,
double quality,
double stability,
double deviation,
int samples,
long capturedAtMs) {
}
public record SolveResult(boolean hasSolution,
boolean ready,
double x,
double z,
double rmse,
double minPairAngleDeg,
double confidence,
int chunkX,
int chunkZ) {
}
}

View file

@ -0,0 +1,197 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TrackedEye.TrackedCapture;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver.CachedThrow;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver.SolveResult;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.model.CapturePolicy;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.EyeOfEnder;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class TriangulatorTracker {
private static final float SEARCH_RANGE = 196.0f;
private static final double CAPTURE_DEVIATION_LIMIT = 0.35;
private static final double CAPTURE_QUALITY_FLOOR = 0.55;
private static final long MISSING_EYE_TIMEOUT_MS = 600L;
private final Map<Integer, TrackedEye> activeEyes = new LinkedHashMap<>();
private final List<CachedThrow> cachedThrows = new ArrayList<>();
private long nextCaptureSerial = 1L;
private long lastCaptureMs = 0L;
private long lastWorldTime = Long.MIN_VALUE;
private long resetMs = 0L;
private String resetReason = "";
public void dropEyes() {
activeEyes.clear();
}
public void clear(String reason, boolean rememberReason, long now) {
activeEyes.clear();
cachedThrows.clear();
nextCaptureSerial = 1L;
lastCaptureMs = 0L;
if (rememberReason) {
resetReason = reason;
resetMs = now;
} else {
resetReason = "";
resetMs = 0L;
}
}
public boolean trackingEye() {
return !activeEyes.isEmpty();
}
public List<CachedThrow> cachedThrows() {
return java.util.Collections.unmodifiableList(cachedThrows);
}
public int throwsCount() {
return cachedThrows.size();
}
public String resetReason() {
return resetReason;
}
public long resetMs() {
return resetMs;
}
private boolean cacheExpired(long now, long lifetimeMs, SolveResult lastResult) {
return lifetimeMs > 0L
&& !cachedThrows.isEmpty()
&& activeEyes.isEmpty()
&& (lastResult == null || !lastResult.hasSolution())
&& lastCaptureMs > 0L
&& now - lastCaptureMs > lifetimeMs;
}
public SolveResult tick(Level level, Player player, CapturePolicy policy, SolveResult lastResult) {
long now = System.currentTimeMillis();
long worldTime = level.getGameTime();
if (lastWorldTime != Long.MIN_VALUE && worldTime + 200L < lastWorldTime) {
activeEyes.clear();
}
lastWorldTime = worldTime;
if (cacheExpired(now, policy.cacheLifetimeMs(), lastResult)) {
clear("Cache expired", true, now);
}
if (level.dimension() != Level.OVERWORLD) {
cleanupMissingEyes(Set.of(), now, policy, lastResult);
return solve(policy);
}
AABB searchBox = player.getBoundingBox().inflate(SEARCH_RANGE);
List<EyeOfEnder> eyes = level.getEntitiesOfClass(
EyeOfEnder.class,
searchBox,
entity -> entity != null && entity.isAlive()
);
Set<Integer> seen = new HashSet<>();
for (EyeOfEnder eye : eyes) {
seen.add(eye.getId());
TrackedEye trackedEye = activeEyes.computeIfAbsent(eye.getId(), TrackedEye::new);
trackedEye.observe(eye, now);
if (!trackedEye.captured()) {
TrackedCapture capture = trackedEye.tryCapture(
policy.minObservationTicks(),
policy.stabilityThreshold(),
CAPTURE_DEVIATION_LIMIT
);
if (capture != null) {
trackedEye.markCaptured();
accept(capture, now, policy, lastResult);
}
}
}
cleanupMissingEyes(seen, now, policy, lastResult);
return solve(policy);
}
private SolveResult solve(CapturePolicy policy) {
return TriangulatorSolver.solve(cachedThrows, policy.minPairAngleDeg());
}
private void cleanupMissingEyes(Set<Integer> seen, long now, CapturePolicy policy, SolveResult lastResult) {
List<Integer> toRemove = new ArrayList<>();
for (Map.Entry<Integer, TrackedEye> entry : activeEyes.entrySet()) {
TrackedEye trackedEye = entry.getValue();
if (seen.contains(entry.getKey())) {
continue;
}
if (!trackedEye.captured()) {
TrackedCapture capture = trackedEye.tryCapture(
policy.minObservationTicks(),
policy.stabilityThreshold() - 0.01,
CAPTURE_DEVIATION_LIMIT * 1.25
);
if (capture != null) {
trackedEye.markCaptured();
accept(capture, now, policy, lastResult);
}
}
if (trackedEye.captured() || now - trackedEye.lastSeenMs() > MISSING_EYE_TIMEOUT_MS) {
toRemove.add(entry.getKey());
}
}
for (Integer id : toRemove) {
activeEyes.remove(id);
}
}
private void accept(TrackedCapture capture, long now, CapturePolicy policy, SolveResult lastResult) {
if (capture.quality() < CAPTURE_QUALITY_FLOOR) {
return;
}
CachedThrow cachedThrow = new CachedThrow(
nextCaptureSerial++,
capture.pointX(),
capture.pointZ(),
capture.dirX(),
capture.dirZ(),
capture.quality(),
capture.stability(),
capture.deviation(),
capture.samples(),
now
);
if (policy.autoReset() && !cachedThrows.isEmpty()) {
SolveResult existing = lastResult != null ? lastResult : solve(policy);
if (existing != null && existing.ready()
&& TriangulatorSolver.distanceToLine(existing.x(), existing.z(), cachedThrow) > policy.autoResetDistance()) {
clear("Different stronghold", true, now);
}
}
cachedThrows.add(cachedThrow);
while (cachedThrows.size() > policy.maxCachedThrows()) {
cachedThrows.remove(0);
}
lastCaptureMs = now;
resetReason = "";
}
}

View file

@ -0,0 +1,197 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver.CachedThrow;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver.SolveResult;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.model.DisplayState;
import dev.loki.lovisual.features.gui.hud.draggable.impl.ScriptedTriangulatorHudPanel;
import java.util.List;
import java.util.Locale;
import static dev.loki.lovisual.features.theme.Theme.theme;
public final class TriangulatorView {
private static final long RESET_FLASH_MS = 3000L;
private int uiHeaderLeft;
private int uiHeaderRight;
private int uiBodyLeft;
private int uiBodyRight;
private int uiOutline;
private int uiText;
private int uiMuted;
private int uiCounter;
private int uiTitleText;
private int uiDivider;
private int uiBlurTint;
private int uiSuccess;
private int uiWarn;
private int uiDanger;
public void update(boolean themeMode, int alpha, int bg, int bg2, int stroke, int text, int muted) {
if (themeMode) {
int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha);
int header = HudRenderUtil.setAlpha(
HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f),
Math.min(255, alpha + 14)
);
int surface = HudRenderUtil.setAlpha(
HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f),
Math.min(255, alpha + 6)
);
int deep = HudRenderUtil.setAlpha(
HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f),
Math.min(255, alpha + 18)
);
uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f);
uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f);
uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f);
uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f);
uiOutline = HudRenderUtil.setAlpha(
HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f),
Math.min(255, Math.max(182, alpha + 36))
);
uiText = theme().textPrimary();
uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f);
uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f);
uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f);
uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.68f);
uiBlurTint = HudRenderUtil.mixColor(
HudRenderUtil.mixColor(uiHeaderRight, uiBodyRight, 0.5f),
theme().accent(),
0.14f
);
} else {
uiHeaderLeft = bg;
uiHeaderRight = bg2;
uiBodyLeft = HudRenderUtil.mixColor(bg, bg2, 0.15f);
uiBodyRight = bg2;
uiOutline = stroke;
uiText = text;
uiTitleText = uiText;
uiMuted = muted;
uiCounter = HudRenderUtil.mixColor(text, 0xFFE1E1FF, 0.20f);
uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.55f);
uiBlurTint = HudRenderUtil.mixColor(uiHeaderLeft, uiBodyRight, 0.5f);
}
uiSuccess = HudRenderUtil.mixColor(uiCounter, 0xFF55FF93, 0.55f);
uiWarn = HudRenderUtil.mixColor(uiCounter, 0xFFFFC85A, 0.55f);
uiDanger = HudRenderUtil.mixColor(uiCounter, 0xFFFF6A6A, 0.62f);
}
public ScriptedTriangulatorHudPanel.Palette palette() {
return new ScriptedTriangulatorHudPanel.Palette(
uiHeaderLeft, uiHeaderRight, uiBodyLeft, uiBodyRight,
uiOutline, uiText, uiMuted, uiCounter,
uiTitleText, uiDivider, uiBlurTint,
uiSuccess, uiWarn, uiDanger
);
}
public int text() {
return uiText;
}
public int muted() {
return uiMuted;
}
public int counter() {
return uiCounter;
}
public int danger() {
return uiDanger;
}
public float blurStrength(boolean themeMode, int blurAlpha) {
return Math.min(1.0f, (blurAlpha / 255.0f) * (themeMode ? 1.15f : 1.0f));
}
public DisplayState status(List<CachedThrow> throwsToShow,
SolveResult result,
boolean preview,
boolean trackingEye,
String resetReason,
long resetMs,
boolean nether) {
long now = System.currentTimeMillis();
if (throwsToShow.isEmpty()) {
if (!resetReason.isEmpty() && now - resetMs <= RESET_FLASH_MS) {
return new DisplayState(resetReason, "Cache cleared", "Waiting for a new eye", "", uiDanger);
}
if (trackingEye) {
return new DisplayState("Tracking eye", "Stabilizing trajectory", "Need one clean capture", "", uiWarn);
}
if (preview) {
return new DisplayState("Solved", TriangulatorSolver.formatCoordinateLine(1288.0, -412.0, nether),
TriangulatorSolver.formatResultLine(1288.0, -412.0, 5.2, nether), "88%", uiSuccess);
}
return new DisplayState("Waiting for eye", "Throw an eye of ender", "Second clean eye unlocks result", "", uiMuted);
}
if (throwsToShow.size() == 1) {
CachedThrow first = throwsToShow.get(0);
return new DisplayState(
trackingEye ? "Tracking next eye" : "Need second eye",
String.format(Locale.ROOT, "Q %s Stability %.3f", TriangulatorSolver.formatPercent(first.quality()), first.stability()),
"Need another angle for solve",
"",
uiWarn
);
}
if (result == null || !result.hasSolution()) {
return new DisplayState(
"Angles too close",
String.format(Locale.ROOT, "Best pair %.2f deg", result == null ? 0.0 : result.minPairAngleDeg()),
"Move farther, then throw again",
"",
uiWarn
);
}
if (!result.ready()) {
return new DisplayState(
"Low correlation",
TriangulatorSolver.formatCoordinateLine(result.x(), result.z(), nether),
String.format(Locale.ROOT, "%s Pair %.2f deg",
TriangulatorSolver.formatResultLine(result.x(), result.z(), result.rmse(), nether), result.minPairAngleDeg()),
TriangulatorSolver.formatPercent(result.confidence()),
uiWarn
);
}
return new DisplayState(
"Solved",
TriangulatorSolver.formatCoordinateLine(result.x(), result.z(), nether),
TriangulatorSolver.formatResultLine(result.x(), result.z(), result.rmse(), nether),
TriangulatorSolver.formatPercent(result.confidence()),
uiSuccess
);
}
public int markerColor(CachedThrow cachedThrow, SolveResult result, int index, int total, double autoResetDistance) {
if (result == null || !result.hasSolution()) {
return index == total - 1 ? uiWarn : uiCounter;
}
double error = TriangulatorSolver.distanceToLine(result.x(), result.z(), cachedThrow);
if (error <= 8.0) {
return uiSuccess;
}
if (error <= autoResetDistance * 0.5) {
return uiWarn;
}
return uiDanger;
}
}

View file

@ -0,0 +1,18 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.model;
/** Tunables the tracker needs for a single tick. */
public record CapturePolicy(int minObservationTicks,
double stabilityThreshold,
double minPairAngleDeg,
boolean autoReset,
double autoResetDistance,
int maxCachedThrows,
long cacheLifetimeMs) {
}

View file

@ -0,0 +1,15 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.model;
public record DisplayState(String statusText,
String primaryLine,
String secondaryLine,
String confidenceText,
int statusColor) {
}

View file

@ -0,0 +1,94 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.render;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver.CachedThrow;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import java.util.List;
/**
* Text measurement pass that computes the animated panel width/height
* targets and shared header metrics for the Triangulator HUD.
*/
public final class TriangulatorMeasure {
static final float CLEAR_BUTTON_SIZE = 9.0f;
static final float HEADER_BUTTON_GAP = 3.0f;
private static final float MIN_WIDTH = 126.0f;
private static final float ROW_STEP = 11.0f;
private static final float CONTENT_START_Y = 23.0f;
private static final float SUMMARY_HEIGHT = 30.5f;
private static final float TITLE_TEXT_X = 22.0f;
private static final float COUNT_LABEL_OFFSET = 28.0f;
public record Metrics(float headerTextH,
float rowTextH,
float maxWidth,
float activeLabelWidth,
float activeCountWidth,
float targetHeight) {
}
private TriangulatorMeasure() {
}
public static Metrics measure(TextRenderer headerTextRenderer,
TextRenderer rowTextRenderer,
float baseScale,
float fontScale,
List<CachedThrow> displayThrows,
String statusText,
String primaryLine,
String secondaryLine,
boolean showClearButton,
boolean showCopyButton) {
headerTextRenderer.begin(fontScale, true, false);
float headerTextH = (float) headerTextRenderer.getHeight(false);
float titleWidth = (float) headerTextRenderer.getWidth("Triangulator", false);
headerTextRenderer.end();
rowTextRenderer.begin(fontScale, true, false);
float rowTextH = (float) rowTextRenderer.getHeight(false);
float maxWidth = Math.max(MIN_WIDTH * baseScale,
(float) rowTextRenderer.getWidth(statusText, false) + 52.0f * baseScale);
maxWidth = Math.max(maxWidth, (float) rowTextRenderer.getWidth(primaryLine, false) + 20.0f * baseScale);
rowTextRenderer.end();
rowTextRenderer.begin(fontScale * 0.92f, false, false);
maxWidth = Math.max(maxWidth, (float) rowTextRenderer.getWidth(secondaryLine, false) + 20.0f * baseScale);
String activeCountText = Integer.toString(displayThrows.size());
float activeLabelWidth = (float) rowTextRenderer.getWidth("Eyes:", false);
float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false);
rowTextRenderer.end();
for (int i = 0; i < displayThrows.size(); i++) {
CachedThrow cachedThrow = displayThrows.get(i);
rowTextRenderer.begin(fontScale, true, false);
float rowWidth = (float) rowTextRenderer.getWidth("Eye #" + (i + 1), false)
+ (float) rowTextRenderer.getWidth(TriangulatorSolver.formatThrowPoint(cachedThrow), false)
+ 36.0f * baseScale;
rowTextRenderer.end();
maxWidth = Math.max(maxWidth, rowWidth);
}
int headerButtons = (showClearButton ? 1 : 0) + (showCopyButton ? 1 : 0);
float headerButtonsWidth = headerButtons == 0 ? 0.0f
: ((CLEAR_BUTTON_SIZE * headerButtons) + (HEADER_BUTTON_GAP * Math.max(0, headerButtons - 1)) + 4.0f) * baseScale;
float headerWidth = (TITLE_TEXT_X * baseScale) + titleWidth
+ activeLabelWidth + activeCountWidth
+ headerButtonsWidth
+ ((COUNT_LABEL_OFFSET + 16.0f) * baseScale);
maxWidth = Math.max(maxWidth, headerWidth);
float rowsHeight = displayThrows.isEmpty() ? 0.0f : displayThrows.size() * (ROW_STEP * baseScale);
float dividerHeight = displayThrows.isEmpty() ? 0.0f : 5.0f * baseScale;
float targetHeight = (CONTENT_START_Y * baseScale) + (SUMMARY_HEIGHT * baseScale) + dividerHeight + rowsHeight;
return new Metrics(headerTextH, rowTextH, maxWidth, activeLabelWidth, activeCountWidth, targetHeight);
}
}

View file

@ -0,0 +1,158 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.render;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
import dev.loki.lovisual.features.gui.hud.HudTextEffects;
import dev.loki.lovisual.features.gui.hud.draggable.impl.ScriptedTriangulatorHudPanel;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver.CachedThrow;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver.SolveResult;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorView;
import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.model.DisplayState;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Util;
import java.util.LinkedHashMap;
import java.util.List;
/**
* Assembles the scripted Triangulator panel: header button hitboxes, row
* data and the final {@link ScriptedTriangulatorHudPanel.Panel} instance.
*/
public final class TriangulatorPainter {
private static final Identifier HEADER_ICON =
Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/coords.png");
private static final float HEADER_HEIGHT = 15.5f;
private static final float CLEAR_BUTTON_RIGHT = 8.0f;
/** Screen-space hitboxes of the header clear/copy buttons. */
public record HeaderButtons(float clearX,
float clearY,
float clearSize,
float copyX,
float copyY,
float copySize) {
public static final HeaderButtons NONE = new HeaderButtons(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
}
private TriangulatorPainter() {
}
public static HeaderButtons placeButtons(TriangulatorMeasure.Metrics metrics,
float drawX,
float drawY,
float drawWidth,
float drawHeight,
float drawBaseScale,
boolean showClearButton,
boolean showCopyButton) {
float buttonCursorRight = CLEAR_BUTTON_RIGHT * drawBaseScale;
float clearX = 0.0f;
float clearY = 0.0f;
float clearSize = 0.0f;
float copyX = 0.0f;
float copyY = 0.0f;
float copySize = 0.0f;
if (showClearButton) {
clearSize = TriangulatorMeasure.CLEAR_BUTTON_SIZE * drawBaseScale;
clearX = drawX + drawWidth - buttonCursorRight - clearSize;
clearY = drawY + (((HEADER_HEIGHT * drawBaseScale) - clearSize) * 0.5f);
buttonCursorRight += clearSize + (TriangulatorMeasure.HEADER_BUTTON_GAP * drawBaseScale);
}
if (showCopyButton) {
copySize = TriangulatorMeasure.CLEAR_BUTTON_SIZE * drawBaseScale;
copyX = drawX + drawWidth - buttonCursorRight - copySize;
copyY = drawY + (((HEADER_HEIGHT * drawBaseScale) - copySize) * 0.5f);
}
return new HeaderButtons(clearX, clearY, clearSize, copyX, copyY, copySize);
}
public static int headerIconColor(int baseColor, HudTextEffects.Effect effect, int effectSpeed) {
if (effect == HudTextEffects.Effect.NONE) {
return baseColor;
}
return HudTextEffects.animatedColor(
baseColor,
effect,
effectSpeed,
(float) (Util.getMillis() / 1000.0),
0.0f
);
}
public static List<LinkedHashMap<String, Object>> buildRows(List<CachedThrow> displayThrows,
SolveResult displayResult,
TriangulatorView view,
double autoResetDistance) {
List<LinkedHashMap<String, Object>> panelRows = ScriptedTriangulatorHudPanel.rows();
for (int i = 0; i < displayThrows.size(); i++) {
CachedThrow cachedThrow = displayThrows.get(i);
int markerColor = view.markerColor(cachedThrow, displayResult, i, displayThrows.size(), autoResetDistance);
panelRows.add(ScriptedTriangulatorHudPanel.row(
"eye:" + cachedThrow.serial(),
"Eye #" + (i + 1),
TriangulatorSolver.formatThrowPoint(cachedThrow),
view.text(),
markerColor,
markerColor,
HudRenderUtil.scaleAlpha(view.muted(), 0.45f),
1.0f
));
}
return panelRows;
}
public static ScriptedTriangulatorHudPanel.Panel panel(TriangulatorView view,
TriangulatorMeasure.Metrics metrics,
float drawX,
float drawY,
float drawWidth,
float drawHeight,
float drawScale,
float drawBaseScale,
float drawFontScale,
int eyeCount,
boolean blur,
float blurStrength,
int headerIconColor,
boolean showClearButton,
boolean showCopyButton,
DisplayState state,
List<LinkedHashMap<String, Object>> rows) {
return new ScriptedTriangulatorHudPanel.Panel(
view.palette(),
drawX,
drawY,
drawWidth,
drawHeight,
drawScale,
drawBaseScale,
drawFontScale,
metrics.headerTextH() * drawScale,
metrics.rowTextH() * drawScale,
metrics.activeLabelWidth() * drawScale,
metrics.activeCountWidth() * drawScale,
eyeCount,
blur,
blurStrength,
ScriptedTriangulatorHudPanel.idString(HEADER_ICON),
headerIconColor,
showClearButton,
view.danger(),
showCopyButton,
view.counter(),
state.statusText(),
state.primaryLine(),
state.secondaryLine(),
state.confidenceText(),
state.statusColor(),
rows
);
}
}

View file

@ -39,6 +39,20 @@ import dev.loki.lovisual.render.engine.renderer.ui.UiItemSubmission;
import dev.loki.lovisual.render.engine.renderer.ui.UiRenderDispatcher; import dev.loki.lovisual.render.engine.renderer.ui.UiRenderDispatcher;
import dev.loki.lovisual.render.engine.postprocess.PostProcessManager; import dev.loki.lovisual.render.engine.postprocess.PostProcessManager;
import dev.loki.lovisual.render.engine.renderer.ui.draw.*; import dev.loki.lovisual.render.engine.renderer.ui.draw.*;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Chamfered2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Rects2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Strokes2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.fx.Blur2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.fx.Effects2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.fx.Glass2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.fx.Smoke2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.media.Items2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.media.Textures2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.path.Circles2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.path.Connectors2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.path.Lines2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.path.Polygons2D;
import dev.loki.lovisual.render.engine.svg.SvgMeshBackend; import dev.loki.lovisual.render.engine.svg.SvgMeshBackend;
import dev.loki.lovisual.render.engine.svg.SvgRegistry; import dev.loki.lovisual.render.engine.svg.SvgRegistry;
import dev.loki.lovisual.render.engine.svg.SvgRenderOptions; import dev.loki.lovisual.render.engine.svg.SvgRenderOptions;
@ -128,13 +142,6 @@ public final class Renderer2D {
final boolean textured; final boolean textured;
final MeshBuilder texturedTriangles; final MeshBuilder texturedTriangles;
final int[] gradientTmp = new int[4]; final int[] gradientTmp = new int[4];
final float[] cornerRadiiTmp = new float[4];
final double[] rectShapeTmp = new double[256];
final double[] connectorTmp = new double[64];
final double[] connectorAnchorTmp = new double[4];
final int[] polygonIndexTmp = new int[64];
final int[] polygonVertexTmp = new int[64];
final double[] progressShapeTmp = new double[256];
double alpha = 1.0; double alpha = 1.0;
public Renderer2D(boolean textured) { public Renderer2D(boolean textured) {
@ -158,6 +165,15 @@ public final class Renderer2D {
return alpha; return alpha;
} }
public boolean isTextured() {
return textured;
}
/** Direct-text triangle mesh; non-null only on the TEXTURE renderer. */
public MeshBuilder texturedMesh() {
return texturedTriangles;
}
public void shape(UiShape shape, UiPaint paint) { public void shape(UiShape shape, UiPaint paint) {
shape(shape, paint, UiStroke.NONE, true); shape(shape, paint, UiStroke.NONE, true);
} }
@ -175,7 +191,7 @@ public final class Renderer2D {
} }
public void box(UiBoxShape box, UiPaint paint, UiStroke stroke, boolean fill) { public void box(UiBoxShape box, UiPaint paint, UiStroke stroke, boolean fill) {
Renderer2DPolygons.box(this, box, paint, stroke, fill); Polygons2D.box(this, box, paint, stroke, fill);
} }
public void shape(UiShape shape, UiPaint paint, UiStroke stroke, boolean fill) { public void shape(UiShape shape, UiPaint paint, UiStroke stroke, boolean fill) {
@ -368,7 +384,7 @@ public final class Renderer2D {
@Nullable String stackCountText, @Nullable String stackCountText,
int durabilityThresholdPercent, int durabilityThresholdPercent,
int durabilityTextColorThresholdPercent) { int durabilityTextColorThresholdPercent) {
Renderer2DItems.submit(this, Items2D.submit(this,
player, stack, x, y, scaleX, scaleY, pivotX, pivotY, pivoted, seed, drawItem, overlayFlags, player, stack, x, y, scaleX, scaleY, pivotX, pivotY, pivoted, seed, drawItem, overlayFlags,
stackCountText, durabilityThresholdPercent, durabilityTextColorThresholdPercent); stackCountText, durabilityThresholdPercent, durabilityTextColorThresholdPercent);
} }
@ -499,7 +515,7 @@ public final class Renderer2D {
double w, double w,
double h, double h,
int argb) { int argb) {
Renderer2DTextures.textureQuad(this, samplerView, sampler, x, y, w, h, argb); Textures2D.textureQuad(this, samplerView, sampler, x, y, w, h, argb);
} }
public void msdfTextureQuad(GpuTextureView samplerView, public void msdfTextureQuad(GpuTextureView samplerView,
@ -528,24 +544,24 @@ public final class Renderer2D {
float pxRange, float pxRange,
int atlasWidth, int atlasWidth,
int atlasHeight) { int atlasHeight) {
Renderer2DTextures.msdfTextureQuad(this, samplerView, sampler, x, y, w, h, Textures2D.msdfTextureQuad(this, samplerView, sampler, x, y, w, h,
topLeft, topRight, bottomRight, bottomLeft, pxRange, atlasWidth, atlasHeight); topLeft, topRight, bottomRight, bottomLeft, pxRange, atlasWidth, atlasHeight);
} }
public void quadBatch(Consumer<QuadBatch> consumer) { public void quadBatch(Consumer<QuadBatch> consumer) {
Renderer2DRects.quadBatch(this, consumer); Rects2D.quadBatch(this, consumer);
} }
public void line(double x1, double y1, double x2, double y2, int argb) { public void line(double x1, double y1, double x2, double y2, int argb) {
Renderer2DLines.line(this, x1, y1, x2, y2, argb); Lines2D.line(this, x1, y1, x2, y2, argb);
} }
public void line(double x1, double y1, double x2, double y2, RenderColor color) { public void line(double x1, double y1, double x2, double y2, RenderColor color) {
Renderer2DLines.line(this, x1, y1, x2, y2, color); Lines2D.line(this, x1, y1, x2, y2, color);
} }
public void boxLines(double x, double y, double width, double height, int argb) { public void boxLines(double x, double y, double width, double height, int argb) {
Renderer2DLines.boxLines(this, x, y, width, height, argb); Lines2D.boxLines(this, x, y, width, height, argb);
} }
public void quad(double x, double y, double width, double height, int argb) { public void quad(double x, double y, double width, double height, int argb) {
@ -553,7 +569,7 @@ public final class Renderer2D {
} }
public void quad(double x, double y, double width, double height, int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { public void quad(double x, double y, double width, double height, int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
Renderer2DRects.quadCorners(this, x, y, width, height, cTopLeft, cTopRight, cBottomRight, cBottomLeft); Rects2D.quadCorners(this, x, y, width, height, cTopLeft, cTopRight, cBottomRight, cBottomLeft);
} }
public void roundedRect(double x, double y, double w, double h, public void roundedRect(double x, double y, double w, double h,
@ -571,7 +587,7 @@ public final class Renderer2D {
} }
public void circle(double cx, double cy, double radius, float softness, int argb) { public void circle(double cx, double cy, double radius, float softness, int argb) {
Renderer2DCircles.circle(this, cx, cy, radius, softness, argb); Circles2D.circle(this, cx, cy, radius, softness, argb);
} }
public void circleSoftShadow(double cx, double cy, double radius, float blur, float innerAlpha, int argb) { public void circleSoftShadow(double cx, double cy, double radius, float blur, float innerAlpha, int argb) {
@ -585,7 +601,7 @@ public final class Renderer2D {
} }
public void circleStroke(double cx, double cy, double radius, double thickness, float softness, int argb) { public void circleStroke(double cx, double cy, double radius, double thickness, float softness, int argb) {
Renderer2DCircles.circleStroke(this, cx, cy, radius, thickness, softness, argb); Circles2D.circleStroke(this, cx, cy, radius, thickness, softness, argb);
} }
public void arcStroke(double cx, double cy, double radius, double thickness, public void arcStroke(double cx, double cy, double radius, double thickness,
@ -651,7 +667,7 @@ public final class Renderer2D {
public void arcStrokeQuad(double cx, double cy, double radius, double thickness, public void arcStrokeQuad(double cx, double cy, double radius, double thickness,
float startAngleDeg, float endAngleDeg, float softness, boolean caps, float startAngleDeg, float endAngleDeg, float softness, boolean caps,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
Renderer2DCircles.arcStrokeQuad(this, cx, cy, radius, thickness, startAngleDeg, endAngleDeg, softness, caps, Circles2D.arcStrokeQuad(this, cx, cy, radius, thickness, startAngleDeg, endAngleDeg, softness, caps,
cTopLeft, cTopRight, cBottomRight, cBottomLeft); cTopLeft, cTopRight, cBottomRight, cBottomLeft);
} }
@ -663,7 +679,7 @@ public final class Renderer2D {
int colorMode, float angularOffset, float angularCycles, int colorMode, float angularOffset, float angularCycles,
float trackAlpha, float bodyAlpha, float headBoost, float trackAlpha, float bodyAlpha, float headBoost,
float trackGlow, boolean caps) { float trackGlow, boolean caps) {
Renderer2DOrbiz.orbizRing(this, cx, cy, radius, thickness, startAngleDeg, sweepAngleDeg, Circles2D.orbizRing(this, cx, cy, radius, thickness, startAngleDeg, sweepAngleDeg,
softness, glowRadius, glowStrength, primaryArgb, secondaryArgb, softness, glowRadius, glowStrength, primaryArgb, secondaryArgb,
colorMode, angularOffset, angularCycles, trackAlpha, bodyAlpha, headBoost, trackGlow, caps); colorMode, angularOffset, angularCycles, trackAlpha, bodyAlpha, headBoost, trackGlow, caps);
} }
@ -676,7 +692,7 @@ public final class Renderer2D {
public void roundedRectStroke(double x, double y, double w, double h, public void roundedRectStroke(double x, double y, double w, double h,
float radius, float softness, float thickness, int argb) { float radius, float softness, float thickness, int argb) {
Renderer2DRectsExtra.roundedRectStroke(this, x, y, w, h, radius, softness, thickness, argb); Strokes2D.roundedRectStroke(this, x, y, w, h, radius, softness, thickness, argb);
} }
public void roundedRectStroke(double x, double y, double w, double h, public void roundedRectStroke(double x, double y, double w, double h,
@ -687,14 +703,14 @@ public final class Renderer2D {
public void roundedRectStrokeAngularGradient(double x, double y, double w, double h, public void roundedRectStrokeAngularGradient(double x, double y, double w, double h,
float radius, float softness, float thickness, float radius, float softness, float thickness,
int startArgb, int endArgb, float offset) { int startArgb, int endArgb, float offset) {
Renderer2DRectsExtra.roundedRectStrokeAngularGradient(this, x, y, w, h, radius, softness, thickness, Strokes2D.roundedRectStrokeAngularGradient(this, x, y, w, h, radius, softness, thickness,
startArgb, endArgb, offset); startArgb, endArgb, offset);
} }
public void roundedRectStrokeCorners(double x, double y, double w, double h, public void roundedRectStrokeCorners(double x, double y, double w, double h,
float radiusTL, float radiusTR, float radiusBR, float radiusBL, float radiusTL, float radiusTR, float radiusBR, float radiusBL,
float softness, float thickness, int argb) { float softness, float thickness, int argb) {
Renderer2DRectsExtra.roundedRectStrokeCorners(this, x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL, Strokes2D.roundedRectStrokeCorners(this, x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL,
softness, thickness, argb); softness, thickness, argb);
} }
@ -723,7 +739,7 @@ public final class Renderer2D {
float radiusTL, float radiusTR, float radiusBR, float radiusBL, float radiusTL, float radiusTR, float radiusBR, float radiusBL,
float softness, float softness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
Renderer2DRects.roundedRectCornersQuad(this, x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL, softness, Rects2D.roundedRectCornersQuad(this, x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL, softness,
cTopLeft, cTopRight, cBottomRight, cBottomLeft); cTopLeft, cTopRight, cBottomRight, cBottomLeft);
} }
@ -772,7 +788,7 @@ public final class Renderer2D {
boolean fromRight, boolean fromRight,
int startArgb, int endArgb, int startArgb, int endArgb,
float angleDeg, float offsetPx) { float angleDeg, float offsetPx) {
Renderer2DRectsExtra.roundedProgressRectGradient(this, x, y, w, h, radius, softness, progress, fromRight, Strokes2D.roundedProgressRectGradient(this, x, y, w, h, radius, softness, progress, fromRight,
startArgb, endArgb, angleDeg, offsetPx); startArgb, endArgb, angleDeg, offsetPx);
} }
@ -800,7 +816,7 @@ public final class Renderer2D {
float flowX, float flowX,
float flowY, float flowY,
float intensity) { float intensity) {
Renderer2DSmokeFill.roundedSmokeFill(this, x, y, w, h, radius, softness, fillRatio, fromRight, Smoke2D.roundedSmokeFill(this, x, y, w, h, radius, softness, fillRatio, fromRight,
firstArgb, secondArgb, thirdArgb, time, smokeScale, smokeMix, octaves, flowX, flowY, intensity); firstArgb, secondArgb, thirdArgb, time, smokeScale, smokeMix, octaves, flowX, flowY, intensity);
} }
@ -849,24 +865,24 @@ public final class Renderer2D {
public void roundedRectGlow(double x, double y, double w, double h, public void roundedRectGlow(double x, double y, double w, double h,
float radius, float softness, float glow, int argb) { float radius, float softness, float glow, int argb) {
Renderer2DRectsGlow.roundedRectGlow(this, x, y, w, h, radius, softness, glow, argb); Effects2D.roundedRectGlow(this, x, y, w, h, radius, softness, glow, argb);
} }
public void roundedRectShadow(double x, double y, double w, double h, public void roundedRectShadow(double x, double y, double w, double h,
float radius, float softness, float spread, int argb) { float radius, float softness, float spread, int argb) {
Renderer2DRectsGlow.roundedRectShadow(this, x, y, w, h, radius, softness, spread, argb); Effects2D.roundedRectShadow(this, x, y, w, h, radius, softness, spread, argb);
} }
public void roundedRectSoftShadow(double x, double y, double w, double h, public void roundedRectSoftShadow(double x, double y, double w, double h,
float radius, float blur, float innerAlpha, int argb) { float radius, float blur, float innerAlpha, int argb) {
Renderer2DRectsGlow.roundedRectSoftShadow(this, x, y, w, h, radius, blur, innerAlpha, argb); Effects2D.roundedRectSoftShadow(this, x, y, w, h, radius, blur, innerAlpha, argb);
} }
public void radialGlowMasked(double x, double y, double w, double h, public void radialGlowMasked(double x, double y, double w, double h,
float radius, float softness, float radius, float softness,
float glowRadius, float cx, float cy, float glowRadius, float cx, float cy,
int argb) { int argb) {
Renderer2DRectsGlow.radialGlowMasked(this, x, y, w, h, radius, softness, glowRadius, cx, cy, argb); Effects2D.radialGlowMasked(this, x, y, w, h, radius, softness, glowRadius, cx, cy, argb);
} }
public void roundedRectGradientQuad(double x, double y, double w, double h, public void roundedRectGradientQuad(double x, double y, double w, double h,
@ -885,14 +901,14 @@ public final class Renderer2D {
double maskX, double maskY, double maskW, double maskH, double maskX, double maskY, double maskW, double maskH,
float radius, float softness, float radius, float softness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
Renderer2DRects.roundedRectMaskedQuad(this, x, y, w, h, maskX, maskY, maskW, maskH, radius, softness, Rects2D.roundedRectMaskedQuad(this, x, y, w, h, maskX, maskY, maskW, maskH, radius, softness,
cTopLeft, cTopRight, cBottomRight, cBottomLeft); cTopLeft, cTopRight, cBottomRight, cBottomLeft);
} }
public void roundedRectStrokeGradientQuad(double x, double y, double w, double h, public void roundedRectStrokeGradientQuad(double x, double y, double w, double h,
float radius, float softness, float thickness, float radius, float softness, float thickness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
Renderer2DRectsExtra.roundedRectStrokeGradientQuad(this, x, y, w, h, radius, softness, thickness, Strokes2D.roundedRectStrokeGradientQuad(this, x, y, w, h, radius, softness, thickness,
cTopLeft, cTopRight, cBottomRight, cBottomLeft); cTopLeft, cTopRight, cBottomRight, cBottomLeft);
} }
@ -907,7 +923,7 @@ public final class Renderer2D {
float radius, float softness, float radius, float softness,
double texX1, double texY1, double texX2, double texY2, double texX1, double texY1, double texX2, double texY2,
int argb, GpuTextureView samplerView, GpuSampler sampler) { int argb, GpuTextureView samplerView, GpuSampler sampler) {
Renderer2DTextures.roundedTexRect(this, x, y, w, h, radius, softness, texX1, texY1, texX2, texY2, Textures2D.roundedTexRect(this, x, y, w, h, radius, softness, texX1, texY1, texX2, texY2,
argb, samplerView, sampler); argb, samplerView, sampler);
} }
@ -915,7 +931,7 @@ public final class Renderer2D {
float radius, float softness, float radius, float softness,
double texX1, double texY1, double texX2, double texY2, double texX1, double texY1, double texX2, double texY2,
int argb, GpuTextureView samplerView, GpuSampler sampler) { int argb, GpuTextureView samplerView, GpuSampler sampler) {
Renderer2DTextures.roundedTexMaskRect(this, x, y, w, h, radius, softness, texX1, texY1, texX2, texY2, Textures2D.roundedTexMaskRect(this, x, y, w, h, radius, softness, texX1, texY1, texX2, texY2,
argb, samplerView, sampler); argb, samplerView, sampler);
} }
@ -1103,12 +1119,12 @@ public final class Renderer2D {
(float) chamferTR, (float) chamferTR,
(float) chamferBR, (float) chamferBR,
(float) chamferBL, (float) chamferBL,
cornerRadiiTmp); Batch2D.CORNER_RADII);
chamferedRectQuadAxes(x, y, w, h, chamferedRectQuadAxes(x, y, w, h,
cornerRadiiTmp[0], cornerRadiiTmp[0], Batch2D.CORNER_RADII[0], Batch2D.CORNER_RADII[0],
cornerRadiiTmp[1], cornerRadiiTmp[1], Batch2D.CORNER_RADII[1], Batch2D.CORNER_RADII[1],
cornerRadiiTmp[2], cornerRadiiTmp[2], Batch2D.CORNER_RADII[2], Batch2D.CORNER_RADII[2],
cornerRadiiTmp[3], cornerRadiiTmp[3], Batch2D.CORNER_RADII[3], Batch2D.CORNER_RADII[3],
cTopLeft, cTopRight, cBottomRight, cBottomLeft); cTopLeft, cTopRight, cBottomRight, cBottomLeft);
} }
@ -1118,7 +1134,7 @@ public final class Renderer2D {
double chamferBRX, double chamferBRY, double chamferBRX, double chamferBRY,
double chamferBLX, double chamferBLY, double chamferBLX, double chamferBLY,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
Renderer2DChamfered.chamferedRectQuadAxes(this, x, y, w, h, Chamfered2D.chamferedRectQuadAxes(this, x, y, w, h,
chamferTLX, chamferTLY, chamferTRX, chamferTRY, chamferBRX, chamferBRY, chamferBLX, chamferBLY, chamferTLX, chamferTLY, chamferTRX, chamferTRY, chamferBRX, chamferBRY, chamferBLX, chamferBLY,
cTopLeft, cTopRight, cBottomRight, cBottomLeft); cTopLeft, cTopRight, cBottomRight, cBottomLeft);
} }
@ -1135,7 +1151,7 @@ public final class Renderer2D {
double chamferTL, double chamferTR, double chamferBR, double chamferBL, double chamferTL, double chamferTR, double chamferBR, double chamferBL,
double thickness, double thickness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
Renderer2DChamfered.chamferedRectStrokeQuad(this, x, y, w, h, Chamfered2D.chamferedRectStrokeQuad(this, x, y, w, h,
chamferTL, chamferTR, chamferBR, chamferBL, thickness, chamferTL, chamferTR, chamferBR, chamferBL, thickness,
cTopLeft, cTopRight, cBottomRight, cBottomLeft); cTopLeft, cTopRight, cBottomRight, cBottomLeft);
} }
@ -1161,35 +1177,35 @@ public final class Renderer2D {
} }
public void notchedRect(double x, double y, double width, double height, public void notchedRect(double x, double y, double width, double height,
double notchWidth, double notchDepth, int argb) { double notchWidth, double notchDepth, int argb) {
int count = buildNotchedRect(rectShapeTmp, x, y, width, height, notchWidth, notchDepth); int count = buildNotchedRect(Batch2D.RECT_SHAPE, x, y, width, height, notchWidth, notchDepth);
Renderer2DPolygons.polygon(this, rectShapeTmp, count, argb); Polygons2D.polygon(this, Batch2D.RECT_SHAPE, count, argb);
} }
public void notchedRectGradient(double x, double y, double width, double height, public void notchedRectGradient(double x, double y, double width, double height,
double notchWidth, double notchDepth, double notchWidth, double notchDepth,
int startArgb, int endArgb, float angleDeg, float offsetPx) { int startArgb, int endArgb, float angleDeg, float offsetPx) {
int count = buildNotchedRect(rectShapeTmp, x, y, width, height, notchWidth, notchDepth); int count = buildNotchedRect(Batch2D.RECT_SHAPE, x, y, width, height, notchWidth, notchDepth);
Renderer2DPolygons.polygonGradient(this, rectShapeTmp, count, x, y, width, height, startArgb, endArgb, angleDeg, offsetPx); Polygons2D.polygonGradient(this, Batch2D.RECT_SHAPE, count, x, y, width, height, startArgb, endArgb, angleDeg, offsetPx);
} }
public void notchedRectStroke(double x, double y, double width, double height, public void notchedRectStroke(double x, double y, double width, double height,
double notchWidth, double notchDepth, double thickness, int argb) { double notchWidth, double notchDepth, double thickness, int argb) {
int count = buildNotchedRect(rectShapeTmp, x, y, width, height, notchWidth, notchDepth); int count = buildNotchedRect(Batch2D.RECT_SHAPE, x, y, width, height, notchWidth, notchDepth);
Renderer2DPolygons.polygonStroke(this, rectShapeTmp, count, thickness, true, argb); Polygons2D.polygonStroke(this, Batch2D.RECT_SHAPE, count, thickness, true, argb);
} }
public void notchedRectStrokeGradient(double x, double y, double width, double height, public void notchedRectStrokeGradient(double x, double y, double width, double height,
double notchWidth, double notchDepth, double thickness, double notchWidth, double notchDepth, double thickness,
int startArgb, int endArgb, float angleDeg, float offsetPx) { int startArgb, int endArgb, float angleDeg, float offsetPx) {
int count = buildNotchedRect(rectShapeTmp, x, y, width, height, notchWidth, notchDepth); int count = buildNotchedRect(Batch2D.RECT_SHAPE, x, y, width, height, notchWidth, notchDepth);
Renderer2DPolygons.polylineLinearGradient(this, rectShapeTmp, count, thickness, true, Polygons2D.polylineLinearGradient(this, Batch2D.RECT_SHAPE, count, thickness, true,
x, y, width, height, startArgb, endArgb, angleDeg, offsetPx, false); x, y, width, height, startArgb, endArgb, angleDeg, offsetPx, false);
} }
public void beveledRect(double x, double y, double width, double height, public void beveledRect(double x, double y, double width, double height,
double bevel, int fillArgb, int highlightArgb, int shadowArgb) { double bevel, int fillArgb, int highlightArgb, int shadowArgb) {
Renderer2DChamfered.beveledRect(this, x, y, width, height, bevel, fillArgb, highlightArgb, shadowArgb); Chamfered2D.beveledRect(this, x, y, width, height, bevel, fillArgb, highlightArgb, shadowArgb);
} }
public void beveledRectStroke(double x, double y, double width, double height, public void beveledRectStroke(double x, double y, double width, double height,
@ -1200,7 +1216,7 @@ public final class Renderer2D {
public void beveledRectGradient(double x, double y, double width, double height, public void beveledRectGradient(double x, double y, double width, double height,
double bevel, int startArgb, int endArgb, double bevel, int startArgb, int endArgb,
int highlightArgb, int shadowArgb, float angleDeg, float offsetPx) { int highlightArgb, int shadowArgb, float angleDeg, float offsetPx) {
Renderer2DChamfered.beveledRectGradient(this, x, y, width, height, bevel, startArgb, endArgb, Chamfered2D.beveledRectGradient(this, x, y, width, height, bevel, startArgb, endArgb,
highlightArgb, shadowArgb, angleDeg, offsetPx); highlightArgb, shadowArgb, angleDeg, offsetPx);
} }
@ -1211,12 +1227,12 @@ public final class Renderer2D {
} }
public void connector(double x1, double y1, double x2, double y2, double thickness, int argb) { public void connector(double x1, double y1, double x2, double y2, double thickness, int argb) {
Renderer2DConnectors.connector(this, x1, y1, x2, y2, thickness, argb); Connectors2D.connector(this, x1, y1, x2, y2, thickness, argb);
} }
public void connectorGradient(double x1, double y1, double x2, double y2, public void connectorGradient(double x1, double y1, double x2, double y2,
double thickness, int startArgb, int endArgb) { double thickness, int startArgb, int endArgb) {
Renderer2DConnectors.connectorGradient(this, x1, y1, x2, y2, thickness, startArgb, endArgb); Connectors2D.connectorGradient(this, x1, y1, x2, y2, thickness, startArgb, endArgb);
} }
public void wire(double x1, double y1, double x2, double y2, double thickness, int argb) { public void wire(double x1, double y1, double x2, double y2, double thickness, int argb) {
@ -1248,13 +1264,13 @@ public final class Renderer2D {
public void roundedRectConnector(double x1, double y1, double w1, double h1, double radius1, public void roundedRectConnector(double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2, double x2, double y2, double w2, double h2, double radius2,
double thickness, int argb) { double thickness, int argb) {
Renderer2DConnectors.roundedRectConnector(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2, thickness, argb); Connectors2D.roundedRectConnector(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2, thickness, argb);
} }
public void roundedRectConnectorGradient(double x1, double y1, double w1, double h1, double radius1, public void roundedRectConnectorGradient(double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2, double x2, double y2, double w2, double h2, double radius2,
double thickness, int startArgb, int endArgb) { double thickness, int startArgb, int endArgb) {
Renderer2DConnectors.roundedRectConnectorGradient(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2, Connectors2D.roundedRectConnectorGradient(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2,
thickness, startArgb, endArgb); thickness, startArgb, endArgb);
} }
@ -1264,13 +1280,13 @@ public final class Renderer2D {
public void roundedRectNodeGraphEdge(double x1, double y1, double w1, double h1, double radius1, public void roundedRectNodeGraphEdge(double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2, double x2, double y2, double w2, double h2, double radius2,
double thickness, int argb) { double thickness, int argb) {
Renderer2DConnectors.roundedRectNodeGraphEdge(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2, thickness, argb); Connectors2D.roundedRectNodeGraphEdge(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2, thickness, argb);
} }
public void roundedRectNodeGraphEdgeGradient(double x1, double y1, double w1, double h1, double radius1, public void roundedRectNodeGraphEdgeGradient(double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2, double x2, double y2, double w2, double h2, double radius2,
double thickness, int startArgb, int endArgb) { double thickness, int startArgb, int endArgb) {
Renderer2DConnectors.roundedRectNodeGraphEdgeGradient(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2, Connectors2D.roundedRectNodeGraphEdgeGradient(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2,
thickness, startArgb, endArgb); thickness, startArgb, endArgb);
} }
@ -1290,14 +1306,14 @@ public final class Renderer2D {
public void roundedRectOrthogonalConnector(double x1, double y1, double w1, double h1, double radius1, public void roundedRectOrthogonalConnector(double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2, double x2, double y2, double w2, double h2, double radius2,
double midX, double thickness, int argb) { double midX, double thickness, int argb) {
Renderer2DConnectors.roundedRectOrthogonalConnector(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2, Connectors2D.roundedRectOrthogonalConnector(this, x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2,
midX, thickness, argb); midX, thickness, argb);
} }
public void roundedRectOrthogonalConnectorGradient(double x1, double y1, double w1, double h1, double radius1, public void roundedRectOrthogonalConnectorGradient(double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2, double x2, double y2, double w2, double h2, double radius2,
double midX, double thickness, int startArgb, int endArgb) { double midX, double thickness, int startArgb, int endArgb) {
Renderer2DConnectors.roundedRectOrthogonalConnectorGradient(this, x1, y1, w1, h1, radius1, Connectors2D.roundedRectOrthogonalConnectorGradient(this, x1, y1, w1, h1, radius1,
x2, y2, w2, h2, radius2, midX, thickness, startArgb, endArgb); x2, y2, w2, h2, radius2, midX, thickness, startArgb, endArgb);
} }
@ -1308,12 +1324,12 @@ public final class Renderer2D {
public void orthogonalConnector(double x1, double y1, double x2, double y2, public void orthogonalConnector(double x1, double y1, double x2, double y2,
double midX, double thickness, int argb) { double midX, double thickness, int argb) {
Renderer2DConnectors.orthogonalConnector(this, x1, y1, x2, y2, midX, thickness, argb); Connectors2D.orthogonalConnector(this, x1, y1, x2, y2, midX, thickness, argb);
} }
public void orthogonalConnectorGradient(double x1, double y1, double x2, double y2, public void orthogonalConnectorGradient(double x1, double y1, double x2, double y2,
double midX, double thickness, int startArgb, int endArgb) { double midX, double thickness, int startArgb, int endArgb) {
Renderer2DConnectors.orthogonalConnectorGradient(this, x1, y1, x2, y2, midX, thickness, startArgb, endArgb); Connectors2D.orthogonalConnectorGradient(this, x1, y1, x2, y2, midX, thickness, startArgb, endArgb);
} }
public void bezierConnector(double x1, double y1, public void bezierConnector(double x1, double y1,
@ -1332,7 +1348,7 @@ public final class Renderer2D {
int segments, int segments,
double thickness, double thickness,
int argb) { int argb) {
Renderer2DConnectors.bezierConnector(this, x1, y1, cx1, cy1, cx2, cy2, x2, y2, segments, thickness, argb); Connectors2D.bezierConnector(this, x1, y1, cx1, cy1, cx2, cy2, x2, y2, segments, thickness, argb);
} }
public void bezierConnectorGradient(double x1, double y1, public void bezierConnectorGradient(double x1, double y1,
@ -1343,33 +1359,33 @@ public final class Renderer2D {
double thickness, double thickness,
int startArgb, int startArgb,
int endArgb) { int endArgb) {
Renderer2DConnectors.bezierConnectorGradient(this, x1, y1, cx1, cy1, cx2, cy2, x2, y2, segments, thickness, startArgb, endArgb); Connectors2D.bezierConnectorGradient(this, x1, y1, cx1, cy1, cx2, cy2, x2, y2, segments, thickness, startArgb, endArgb);
} }
public void nodeGraphEdge(double x1, double y1, double x2, double y2, double thickness, int argb) { public void nodeGraphEdge(double x1, double y1, double x2, double y2, double thickness, int argb) {
Renderer2DConnectors.nodeGraphEdge(this, x1, y1, x2, y2, thickness, argb); Connectors2D.nodeGraphEdge(this, x1, y1, x2, y2, thickness, argb);
} }
public void nodeGraphEdgeGradient(double x1, double y1, double x2, double y2, public void nodeGraphEdgeGradient(double x1, double y1, double x2, double y2,
double thickness, int startArgb, int endArgb) { double thickness, int startArgb, int endArgb) {
Renderer2DConnectors.nodeGraphEdgeGradient(this, x1, y1, x2, y2, thickness, startArgb, endArgb); Connectors2D.nodeGraphEdgeGradient(this, x1, y1, x2, y2, thickness, startArgb, endArgb);
} }
public void spline(double[] points, int pointCount, double thickness, boolean closed, int argb) { public void spline(double[] points, int pointCount, double thickness, boolean closed, int argb) {
if (points == null || pointCount < 2) return; if (points == null || pointCount < 2) return;
Renderer2DPolygons.polyline(this, points, pointCount, thickness, closed, argb, true); Polygons2D.polyline(this, points, pointCount, thickness, closed, argb, true);
} }
public void splineGradient(double[] points, int pointCount, double thickness, boolean closed, public void splineGradient(double[] points, int pointCount, double thickness, boolean closed,
int startArgb, int endArgb) { int startArgb, int endArgb) {
if (points == null || pointCount < 2) return; if (points == null || pointCount < 2) return;
Renderer2DPolygons.polylineGradient(this, points, pointCount, thickness, closed, startArgb, endArgb, true); Polygons2D.polylineGradient(this, points, pointCount, thickness, closed, startArgb, endArgb, true);
} }
public void texQuad(double x, double y, double width, double height, public void texQuad(double x, double y, double width, double height,
double texX1, double texY1, double texX2, double texY2, double texX1, double texY1, double texX2, double texY2,
int argb) { int argb) {
Renderer2DTextures.texQuad(this, x, y, width, height, texX1, texY1, texX2, texY2, argb); Textures2D.texQuad(this, x, y, width, height, texX1, texY1, texX2, texY2, argb);
} }
public void liquidGlassCircle(double cx, double cy, double radius, public void liquidGlassCircle(double cx, double cy, double radius,
@ -1745,31 +1761,31 @@ public final class Renderer2D {
float fresnelMix, float fresnelMix,
float distortPx, float distortPx,
float squirclePower) { float squirclePower) {
Renderer2DLiquidGlass.liquidGlassRectCorners(this, x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL, softness, Glass2D.liquidGlassRectCorners(this, x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL, softness,
tintArgb, glassAlpha, blurAlpha, fresnelPower, fresnelAlpha, baseAlpha, fresnelMix, distortPx, squirclePower); tintArgb, glassAlpha, blurAlpha, fresnelPower, fresnelAlpha, baseAlpha, fresnelMix, distortPx, squirclePower);
} }
public void blurRect(double x, double y, double w, double h, public void blurRect(double x, double y, double w, double h,
float radius, float quality, float brightness, float alpha, int ignoredTintRgb) { float radius, float quality, float brightness, float alpha, int ignoredTintRgb) {
Renderer2DBlur.blurRect(this, x, y, w, h, radius, quality, brightness, alpha, ignoredTintRgb, UiBatchType.BLUR); Blur2D.blurRect(this, x, y, w, h, radius, quality, brightness, alpha, ignoredTintRgb);
} }
public void glassBlurRect(double x, double y, double w, double h, public void glassBlurRect(double x, double y, double w, double h,
float radius, float quality, float brightness, float alpha, int ignoredTintRgb) { float radius, float quality, float brightness, float alpha, int ignoredTintRgb) {
Renderer2DBlur.glassBlurRect(this, x, y, w, h, radius, quality, brightness, alpha, ignoredTintRgb); Blur2D.glassBlurRect(this, x, y, w, h, radius, quality, brightness, alpha, ignoredTintRgb);
} }
public void blurChamferedRect(double x, double y, double w, double h, public void blurChamferedRect(double x, double y, double w, double h,
double chamfer, float quality, float brightness, float alpha, int ignoredTintRgb) { double chamfer, float quality, float brightness, float alpha, int ignoredTintRgb) {
Renderer2DBlur.blurChamferedRect(this, x, y, w, h, chamfer, quality, brightness, alpha, ignoredTintRgb); Blur2D.blurChamferedRect(this, x, y, w, h, chamfer, quality, brightness, alpha, ignoredTintRgb);
} }
public void blurComposite(Consumer<BlurComposite> consumer) { public void blurComposite(Consumer<BlurComposite> consumer) {
Renderer2DBlur.blurComposite(this, consumer); Blur2D.blurComposite(this, consumer);
} }
public void polygon(double[] points, int pointCount, int argb) { public void polygon(double[] points, int pointCount, int argb) {
Renderer2DPolygons.polygon(this, points, pointCount, argb); Polygons2D.polygon(this, points, pointCount, argb);
} }
// ---- Package-private plumbing for helpers in the same package ------------ // ---- Package-private plumbing for helpers in the same package ------------
@ -2077,7 +2093,7 @@ public final class Renderer2D {
private final MeshBuilder mesh; private final MeshBuilder mesh;
private final float globalAlpha; private final float globalAlpha;
BlurComposite(MeshBuilder mesh, float globalAlpha) { public BlurComposite(MeshBuilder mesh, float globalAlpha) {
this.mesh = mesh; this.mesh = mesh;
this.globalAlpha = globalAlpha; this.globalAlpha = globalAlpha;
} }
@ -2132,7 +2148,7 @@ public final class Renderer2D {
public static final class QuadBatch { public static final class QuadBatch {
private final MeshBuilder mesh; private final MeshBuilder mesh;
QuadBatch(MeshBuilder mesh) { public QuadBatch(MeshBuilder mesh) {
this.mesh = mesh; this.mesh = mesh;
} }

View file

@ -1,157 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.postprocess.PostProcessManager;
import dev.loki.lovisual.render.engine.renderer.ui.BlurSource;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiEffectSpec;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
import java.util.function.Consumer;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clamp01;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampChamfer;
final class Renderer2DBlur {
private Renderer2DBlur() {
}
static Renderer2D.BlurQuality legacyBlurQuality(float legacyQuality) {
if (legacyQuality >= 32.0f) return Renderer2D.BlurQuality.ULTRA;
if (legacyQuality >= 12.0f) return Renderer2D.BlurQuality.HIGH;
if (legacyQuality >= 5.0f) return Renderer2D.BlurQuality.MEDIUM;
return Renderer2D.BlurQuality.LOW;
}
static float legacyKawaseOffset(float legacyQuality) {
if (!Float.isFinite(legacyQuality)) return Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
return Mth.clamp(legacyQuality / 18.0f, 0.85f, 3.25f);
}
static BlurSource getBlurSource(Renderer2D r) {
Minecraft mc = Minecraft.getInstance();
if (mc == null) return null;
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
if (fb == null) return null;
GpuTextureView src = fb.getColorTextureView();
if (src == null) return null;
GpuSampler sampler = PostProcessManager.getSampler();
if (sampler == null) return null;
return new BlurSource(src, sampler);
}
static void blurRect(Renderer2D r, double x, double y, double w, double h,
float radius, float quality, float brightness, float alpha, int ignoredTintRgb,
UiBatchType batchType) {
if (w <= 0.0 || h <= 0.0) return;
if (alpha <= 0.001f) return;
r.effect(UiEffectSpec.blur(UiShape.roundedRect(x, y, w, h, radius), radius, ignoredTintRgb));
BlurSource source = getBlurSource(r);
if (source == null) return;
boolean auto = r.beginAutoBatch();
try {
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreateBlur(batchType, source.view, source.sampler,
legacyBlurQuality(quality), legacyKawaseOffset(quality));
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.alpha = 1.0;
float finalAlpha = (float) (alpha * r.alpha);
mesh.ensureQuadCapacity();
int i1 = mesh.vec2(x, y).local2(x, y).color(255, 255, 255, 255).vec4(x, y, w, h).vec4(radius, quality, brightness, finalAlpha).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(255, 255, 255, 255).vec4(x, y, w, h).vec4(radius, quality, brightness, finalAlpha).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(255, 255, 255, 255).vec4(x, y, w, h).vec4(radius, quality, brightness, finalAlpha).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(255, 255, 255, 255).vec4(x, y, w, h).vec4(radius, quality, brightness, finalAlpha).next();
mesh.quad(i1, i2, i3, i4);
} finally {
r.endAutoBatch(auto);
}
}
static void glassBlurRect(Renderer2D r, double x, double y, double w, double h,
float radius, float quality, float brightness, float alpha, int ignoredTintRgb) {
if (w <= 0.0 || h <= 0.0) return;
if (alpha <= 0.001f) return;
r.effect(UiEffectSpec.blur(UiShape.roundedRect(x, y, w, h, radius), radius, ignoredTintRgb));
BlurSource source = getBlurSource(r);
if (source == null) return;
boolean auto = r.beginAutoBatch();
try {
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreateBlur(UiBatchType.GLASS_BLUR, source.view, source.sampler,
Renderer2D.BlurQuality.HIGH, legacyKawaseOffset(quality));
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.alpha = 1.0;
float finalAlpha = clamp01((float) (alpha * r.alpha));
if (finalAlpha <= 0.001f) return;
int a = Math.max(0, Math.min(255, Math.round(finalAlpha * 255.0f)));
float smoothness = 2.0f;
float rad = Math.max(0.0f, radius);
mesh.ensureQuadCapacity();
int i1 = mesh.vec2(x, y).local2(x, y).color(255, 255, 255, a)
.vec4(x, y, w, h).vec4(rad, rad, rad, rad).vec4(quality, brightness, smoothness, 0.0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(255, 255, 255, a)
.vec4(x, y, w, h).vec4(rad, rad, rad, rad).vec4(quality, brightness, smoothness, 0.0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(255, 255, 255, a)
.vec4(x, y, w, h).vec4(rad, rad, rad, rad).vec4(quality, brightness, smoothness, 0.0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(255, 255, 255, a)
.vec4(x, y, w, h).vec4(rad, rad, rad, rad).vec4(quality, brightness, smoothness, 0.0f).next();
mesh.quad(i1, i2, i3, i4);
} finally {
r.endAutoBatch(auto);
}
}
static void blurChamferedRect(Renderer2D r, double x, double y, double w, double h,
double chamfer, float quality, float brightness, float alpha, int ignoredTintRgb) {
if (w <= 0.0 || h <= 0.0) return;
if (alpha <= 0.001f) return;
float cut = clampChamfer(chamfer, w, h);
if (cut <= 0.0f) {
r.blurRect(x, y, w, h, 0.0f, quality, brightness, alpha, ignoredTintRgb);
return;
}
r.blurRect(x, y, w, h, -cut, quality, brightness, alpha, ignoredTintRgb);
}
static void blurComposite(Renderer2D r, Consumer<Renderer2D.BlurComposite> consumer) {
if (consumer == null) return;
BlurSource source = getBlurSource(r);
if (source == null) return;
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreateBlur(UiBatchType.BLUR_COMPLEX, source.view, source.sampler,
Renderer2D.DEFAULT_BLUR_QUALITY, Renderer2D.DEFAULT_KAWASE_OFFSET_PX);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.alpha = 1.0;
try {
consumer.accept(new Renderer2D.BlurComposite(mesh, (float) r.alpha));
} finally {
r.endAutoBatch(auto);
}
}
}

View file

@ -1,175 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampChamferAxis;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeChamfers;
final class Renderer2DChamfered {
private Renderer2DChamfered() {
}
static void chamferedRectQuadAxes(Renderer2D r, double x, double y, double w, double h,
double chamferTLX, double chamferTLY,
double chamferTRX, double chamferTRY,
double chamferBRX, double chamferBRY,
double chamferBLX, double chamferBLY,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
r.shape(UiShape.chamferedRectAxes(x, y, w, h, chamferTLX, chamferTLY, chamferTRX, chamferTRY, chamferBRX, chamferBRY, chamferBLX, chamferBLY),
UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.CHAMFERED, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int tlA = (cTopLeft >>> 24) & 0xFF;
int tlR = (cTopLeft >>> 16) & 0xFF;
int tlG = (cTopLeft >>> 8) & 0xFF;
int tlB = cTopLeft & 0xFF;
int trA = (cTopRight >>> 24) & 0xFF;
int trR = (cTopRight >>> 16) & 0xFF;
int trG = (cTopRight >>> 8) & 0xFF;
int trB = cTopRight & 0xFF;
int brA = (cBottomRight >>> 24) & 0xFF;
int brR = (cBottomRight >>> 16) & 0xFF;
int brG = (cBottomRight >>> 8) & 0xFF;
int brB = cBottomRight & 0xFF;
int blA = (cBottomLeft >>> 24) & 0xFF;
int blR = (cBottomLeft >>> 16) & 0xFF;
int blG = (cBottomLeft >>> 8) & 0xFF;
int blB = cBottomLeft & 0xFF;
float tlX = clampChamferAxis(chamferTLX, w);
float trX = clampChamferAxis(chamferTRX, w);
float brX = clampChamferAxis(chamferBRX, w);
float blX = clampChamferAxis(chamferBLX, w);
float tlY = clampChamferAxis(chamferTLY, h);
float trY = clampChamferAxis(chamferTRY, h);
float brY = clampChamferAxis(chamferBRY, h);
float blY = clampChamferAxis(chamferBLY, h);
int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(x, y, w, h).vec4(tlX, trX, brX, blX).vec4(tlY, trY, brY, blY).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(x, y, w, h).vec4(tlX, trX, brX, blX).vec4(tlY, trY, brY, blY).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h).vec4(tlX, trX, brX, blX).vec4(tlY, trY, brY, blY).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h).vec4(tlX, trX, brX, blX).vec4(tlY, trY, brY, blY).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
static void chamferedRectStrokeQuad(Renderer2D r, double x, double y, double w, double h,
double chamferTL, double chamferTR, double chamferBR, double chamferBL,
double thickness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
r.shapeStroke(UiShape.chamferedRect(x, y, w, h, chamferTL, chamferTR, chamferBR, chamferBL),
UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft), UiStroke.of(thickness));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.CHAMFERED_STROKE, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int tlA = (cTopLeft >>> 24) & 0xFF;
int tlR = (cTopLeft >>> 16) & 0xFF;
int tlG = (cTopLeft >>> 8) & 0xFF;
int tlB = cTopLeft & 0xFF;
int trA = (cTopRight >>> 24) & 0xFF;
int trR = (cTopRight >>> 16) & 0xFF;
int trG = (cTopRight >>> 8) & 0xFF;
int trB = cTopRight & 0xFF;
int brA = (cBottomRight >>> 24) & 0xFF;
int brR = (cBottomRight >>> 16) & 0xFF;
int brG = (cBottomRight >>> 8) & 0xFF;
int brB = cBottomRight & 0xFF;
int blA = (cBottomLeft >>> 24) & 0xFF;
int blR = (cBottomLeft >>> 16) & 0xFF;
int blG = (cBottomLeft >>> 8) & 0xFF;
int blB = cBottomLeft & 0xFF;
float[] cornerRadiiTmp = r.cornerRadiiTmp;
normalizeChamfers(w, h,
(float) chamferTL,
(float) chamferTR,
(float) chamferBR,
(float) chamferBL,
cornerRadiiTmp);
float stroke = (float) Math.max(0.0, thickness);
int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(stroke, 0f, 0f, 0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(stroke, 0f, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(stroke, 0f, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(stroke, 0f, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
static void beveledRect(Renderer2D r, double x, double y, double width, double height,
double bevel, int fillArgb, int highlightArgb, int shadowArgb) {
r.chamferedRect(x, y, width, height, bevel, fillArgb);
double t = Math.max(1.0, Math.min(Math.min(Math.abs(width), Math.abs(height)) * 0.25, bevel));
double[] connectorTmp = r.connectorTmp;
connectorTmp[0] = x + bevel;
connectorTmp[1] = y;
connectorTmp[2] = x + width - bevel;
connectorTmp[3] = y;
connectorTmp[4] = x + width;
connectorTmp[5] = y + bevel;
Renderer2DPolygons.polyline(r, connectorTmp, 3, t, false, highlightArgb, false);
connectorTmp[0] = x + width;
connectorTmp[1] = y + height - bevel;
connectorTmp[2] = x + width - bevel;
connectorTmp[3] = y + height;
connectorTmp[4] = x + bevel;
connectorTmp[5] = y + height;
connectorTmp[6] = x;
connectorTmp[7] = y + height - bevel;
Renderer2DPolygons.polyline(r, connectorTmp, 4, t, false, shadowArgb, false);
}
static void beveledRectGradient(Renderer2D r, double x, double y, double width, double height,
double bevel, int startArgb, int endArgb,
int highlightArgb, int shadowArgb, float angleDeg, float offsetPx) {
r.chamferedRectGradient(x, y, width, height, bevel, startArgb, endArgb, angleDeg, offsetPx);
double t = Math.max(1.0, Math.min(Math.min(Math.abs(width), Math.abs(height)) * 0.25, bevel));
double[] connectorTmp = r.connectorTmp;
connectorTmp[0] = x + bevel;
connectorTmp[1] = y;
connectorTmp[2] = x + width - bevel;
connectorTmp[3] = y;
connectorTmp[4] = x + width;
connectorTmp[5] = y + bevel;
Renderer2DPolygons.polyline(r, connectorTmp, 3, t, false, highlightArgb, false);
connectorTmp[0] = x + width;
connectorTmp[1] = y + height - bevel;
connectorTmp[2] = x + width - bevel;
connectorTmp[3] = y + height;
connectorTmp[4] = x + bevel;
connectorTmp[5] = y + height;
connectorTmp[6] = x;
connectorTmp[7] = y + height - bevel;
Renderer2DPolygons.polyline(r, connectorTmp, 4, t, false, shadowArgb, false);
}
}

View file

@ -1,152 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.util.Mth;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeDegrees;
final class Renderer2DCircles {
private Renderer2DCircles() {
}
static void circle(Renderer2D r, double cx, double cy, double radius, float softness, int argb) {
r.shape(UiShape.circle(cx, cy, radius), UiPaint.solid(argb));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.CIRCLE, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF;
double outerRadius = radius + Math.max(0.0f, softness);
double x = cx - outerRadius;
double y = cy - outerRadius;
double w = outerRadius * 2.0;
double h = outerRadius * 2.0;
int i1 = mesh.vec2(x, y).local2(x, y).color(red, green, blue, a).vec4(x, y, w, h).vec4((float) radius, softness, 0f, 0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4((float) radius, softness, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4((float) radius, softness, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4((float) radius, softness, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
static void circleStroke(Renderer2D r, double cx, double cy, double radius, double thickness, float softness, int argb) {
r.shapeStroke(UiShape.circle(cx, cy, radius), UiPaint.solid(argb), UiStroke.of(thickness));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.CIRCLE, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF;
float stroke = (float) Math.max(0.0, thickness);
double outerRadius = radius + stroke * 0.5 + softness;
double x = cx - outerRadius;
double y = cy - outerRadius;
double w = outerRadius * 2.0;
double h = outerRadius * 2.0;
int i1 = mesh.vec2(x, y).local2(x, y).color(red, green, blue, a).vec4(x, y, w, h).vec4((float) radius, softness, stroke, 0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4((float) radius, softness, stroke, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4((float) radius, softness, stroke, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4((float) radius, softness, stroke, 0f).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
static void arcStrokeQuad(Renderer2D r, double cx, double cy, double radius, double thickness,
float startAngleDeg, float endAngleDeg, float softness, boolean caps,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
float start = normalizeDegrees(startAngleDeg);
float end = normalizeDegrees(endAngleDeg);
float sweep = end - start;
if (sweep <= 0.0f) {
sweep += 360.0f;
}
if (sweep <= 0.001f) {
return;
}
r.shapeStroke(UiShape.arc(cx, cy, radius, start, end), UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft), UiStroke.of(thickness));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ARC, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
float stroke = (float) Math.max(0.0, thickness);
double outerRadius = radius + stroke * 0.5 + softness;
double x = cx - outerRadius;
double y = cy - outerRadius;
double w = outerRadius * 2.0;
double h = outerRadius * 2.0;
int tlA = (cTopLeft >>> 24) & 0xFF;
int tlR = (cTopLeft >>> 16) & 0xFF;
int tlG = (cTopLeft >>> 8) & 0xFF;
int tlB = cTopLeft & 0xFF;
int trA = (cTopRight >>> 24) & 0xFF;
int trR = (cTopRight >>> 16) & 0xFF;
int trG = (cTopRight >>> 8) & 0xFF;
int trB = cTopRight & 0xFF;
int brA = (cBottomRight >>> 24) & 0xFF;
int brR = (cBottomRight >>> 16) & 0xFF;
int brG = (cBottomRight >>> 8) & 0xFF;
int brB = cBottomRight & 0xFF;
int blA = (cBottomLeft >>> 24) & 0xFF;
int blR = (cBottomLeft >>> 16) & 0xFF;
int blG = (cBottomLeft >>> 8) & 0xFF;
int blB = cBottomLeft & 0xFF;
int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(x, y, w, h)
.vec4((float) radius, softness, stroke, start).vec4(end, caps ? 1f : 0f, 0f, 0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(x, y, w, h)
.vec4((float) radius, softness, stroke, start).vec4(end, caps ? 1f : 0f, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h)
.vec4((float) radius, softness, stroke, start).vec4(end, caps ? 1f : 0f, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h)
.vec4((float) radius, softness, stroke, start).vec4(end, caps ? 1f : 0f, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
}

View file

@ -1,155 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.buildBezier;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.roundedRectAnchor;
final class Renderer2DConnectors {
private Renderer2DConnectors() {
}
static void connector(Renderer2D r, double x1, double y1, double x2, double y2, double thickness, int argb) {
double[] c = r.connectorTmp;
c[0] = x1;
c[1] = y1;
c[2] = x2;
c[3] = y2;
Renderer2DPolygons.polyline(r, c, 2, thickness, false, argb, true);
}
static void connectorGradient(Renderer2D r, double x1, double y1, double x2, double y2,
double thickness, int startArgb, int endArgb) {
double[] c = r.connectorTmp;
c[0] = x1;
c[1] = y1;
c[2] = x2;
c[3] = y2;
Renderer2DPolygons.polylineGradient(r, c, 2, thickness, false, startArgb, endArgb, true);
}
static void orthogonalConnector(Renderer2D r, double x1, double y1, double x2, double y2,
double midX, double thickness, int argb) {
double[] c = r.connectorTmp;
c[0] = x1;
c[1] = y1;
c[2] = midX;
c[3] = y1;
c[4] = midX;
c[5] = y2;
c[6] = x2;
c[7] = y2;
Renderer2DPolygons.polyline(r, c, 4, thickness, false, argb, true);
}
static void orthogonalConnectorGradient(Renderer2D r, double x1, double y1, double x2, double y2,
double midX, double thickness, int startArgb, int endArgb) {
double[] c = r.connectorTmp;
c[0] = x1;
c[1] = y1;
c[2] = midX;
c[3] = y1;
c[4] = midX;
c[5] = y2;
c[6] = x2;
c[7] = y2;
Renderer2DPolygons.polylineGradient(r, c, 4, thickness, false, startArgb, endArgb, true);
}
static void bezierConnector(Renderer2D r, double x1, double y1,
double cx1, double cy1,
double cx2, double cy2,
double x2, double y2,
int segments,
double thickness,
int argb) {
double[] c = r.connectorTmp;
int count = buildBezier(c, x1, y1, cx1, cy1, cx2, cy2, x2, y2, segments);
Renderer2DPolygons.polyline(r, c, count, thickness, false, argb, false);
}
static void bezierConnectorGradient(Renderer2D r, double x1, double y1,
double cx1, double cy1,
double cx2, double cy2,
double x2, double y2,
int segments,
double thickness,
int startArgb,
int endArgb) {
double[] c = r.connectorTmp;
int count = buildBezier(c, x1, y1, cx1, cy1, cx2, cy2, x2, y2, segments);
Renderer2DPolygons.polylineGradient(r, c, count, thickness, false, startArgb, endArgb, false);
}
static void nodeGraphEdge(Renderer2D r, double x1, double y1, double x2, double y2, double thickness, int argb) {
double dx = Math.abs(x2 - x1);
double c = Math.max(24.0, dx * 0.5);
bezierConnector(r, x1, y1, x1 + c, y1, x2 - c, y2, x2, y2, 20, thickness, argb);
}
static void nodeGraphEdgeGradient(Renderer2D r, double x1, double y1, double x2, double y2,
double thickness, int startArgb, int endArgb) {
double dx = Math.abs(x2 - x1);
double c = Math.max(24.0, dx * 0.5);
bezierConnectorGradient(r, x1, y1, x1 + c, y1, x2 - c, y2, x2, y2, 20, thickness, startArgb, endArgb);
}
static void roundedRectConnector(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double thickness, int argb) {
double[] a = r.connectorAnchorTmp;
roundedRectAnchor(a, 0, x1, y1, w1, h1, radius1, x2 + w2 * 0.5, y2 + h2 * 0.5);
roundedRectAnchor(a, 2, x2, y2, w2, h2, radius2, x1 + w1 * 0.5, y1 + h1 * 0.5);
connector(r, a[0], a[1], a[2], a[3], thickness, argb);
}
static void roundedRectConnectorGradient(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double thickness, int startArgb, int endArgb) {
double[] a = r.connectorAnchorTmp;
roundedRectAnchor(a, 0, x1, y1, w1, h1, radius1, x2 + w2 * 0.5, y2 + h2 * 0.5);
roundedRectAnchor(a, 2, x2, y2, w2, h2, radius2, x1 + w1 * 0.5, y1 + h1 * 0.5);
connectorGradient(r, a[0], a[1], a[2], a[3], thickness, startArgb, endArgb);
}
static void roundedRectNodeGraphEdge(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double thickness, int argb) {
double[] a = r.connectorAnchorTmp;
roundedRectAnchor(a, 0, x1, y1, w1, h1, radius1, x2 + w2 * 0.5, y2 + h2 * 0.5);
roundedRectAnchor(a, 2, x2, y2, w2, h2, radius2, x1 + w1 * 0.5, y1 + h1 * 0.5);
nodeGraphEdge(r, a[0], a[1], a[2], a[3], thickness, argb);
}
static void roundedRectNodeGraphEdgeGradient(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double thickness, int startArgb, int endArgb) {
double[] a = r.connectorAnchorTmp;
roundedRectAnchor(a, 0, x1, y1, w1, h1, radius1, x2 + w2 * 0.5, y2 + h2 * 0.5);
roundedRectAnchor(a, 2, x2, y2, w2, h2, radius2, x1 + w1 * 0.5, y1 + h1 * 0.5);
nodeGraphEdgeGradient(r, a[0], a[1], a[2], a[3], thickness, startArgb, endArgb);
}
static void roundedRectOrthogonalConnector(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double midX, double thickness, int argb) {
double[] a = r.connectorAnchorTmp;
roundedRectAnchor(a, 0, x1, y1, w1, h1, radius1, x2 + w2 * 0.5, y2 + h2 * 0.5);
roundedRectAnchor(a, 2, x2, y2, w2, h2, radius2, x1 + w1 * 0.5, y1 + h1 * 0.5);
orthogonalConnector(r, a[0], a[1], a[2], a[3], midX, thickness, argb);
}
static void roundedRectOrthogonalConnectorGradient(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double midX, double thickness, int startArgb, int endArgb) {
double[] a = r.connectorAnchorTmp;
roundedRectAnchor(a, 0, x1, y1, w1, h1, radius1, x2 + w2 * 0.5, y2 + h2 * 0.5);
roundedRectAnchor(a, 2, x2, y2, w2, h2, radius2, x1 + w1 * 0.5, y1 + h1 * 0.5);
orthogonalConnectorGradient(r, a[0], a[1], a[2], a[3], midX, thickness, startArgb, endArgb);
}
}

View file

@ -1,55 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import dev.loki.lovisual.render.engine.renderer.ui.UiItemSubmission;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
final class Renderer2DItems {
private Renderer2DItems() {
}
static void submit(Renderer2D r,
@Nullable LocalPlayer player,
ItemStack stack,
double x,
double y,
float scaleX,
float scaleY,
float pivotX,
float pivotY,
boolean pivoted,
int seed,
boolean drawItem,
int overlayFlags,
@Nullable String stackCountText,
int durabilityThresholdPercent,
int durabilityTextColorThresholdPercent) {
UiItemSubmission.submit(
r.textured,
r.alpha,
player,
stack,
x,
y,
scaleX,
scaleY,
pivotX,
pivotY,
pivoted,
seed,
drawItem,
overlayFlags,
stackCountText,
durabilityThresholdPercent,
durabilityTextColorThresholdPercent
);
}
}

View file

@ -1,95 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
final class Renderer2DLines {
private Renderer2DLines() {
}
static void line(Renderer2D r, double x1, double y1, double x2, double y2, int argb) {
double[] p = {x1, y1, x2, y2};
r.path(UiShape.polyline(p, 2, false), UiPaint.solid(argb), UiStroke.of(1.0), false);
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.LINES, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureLineCapacity();
int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF;
int i1 = mesh.vec2(x1, y1).color(red, green, blue, a).next();
int i2 = mesh.vec2(x2, y2).color(red, green, blue, a).next();
mesh.line(i1, i2);
r.endAutoBatch(auto);
}
static void line(Renderer2D r, double x1, double y1, double x2, double y2, RenderColor color) {
double[] p = {x1, y1, x2, y2};
r.path(UiShape.polyline(p, 2, false), UiPaint.solid(color.argb()), UiStroke.of(1.0), false);
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.LINES, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureLineCapacity();
int i1 = mesh.vec2(x1, y1).color(color.r, color.g, color.b, color.a).next();
int i2 = mesh.vec2(x2, y2).color(color.r, color.g, color.b, color.a).next();
mesh.line(i1, i2);
r.endAutoBatch(auto);
}
static void boxLines(Renderer2D r, double x, double y, double width, double height, int argb) {
r.shapeStroke(UiShape.rect(x, y, width, height), UiPaint.solid(argb), UiStroke.of(1.0));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.LINES, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureCapacity(4, 8);
int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF;
int i1 = mesh.vec2(x, y).color(red, green, blue, a).next();
int i2 = mesh.vec2(x, y + height).color(red, green, blue, a).next();
int i3 = mesh.vec2(x + width, y + height).color(red, green, blue, a).next();
int i4 = mesh.vec2(x + width, y).color(red, green, blue, a).next();
mesh.line(i1, i2);
mesh.line(i2, i3);
mesh.line(i3, i4);
mesh.line(i4, i1);
r.endAutoBatch(auto);
}
}

View file

@ -1,109 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.postprocess.PostProcessManager;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiEffectSpec;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.client.Minecraft;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clamp01;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeCornerRadii;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.packLiquidGlassPayload;
final class Renderer2DLiquidGlass {
private Renderer2DLiquidGlass() {
}
static void liquidGlassRectCorners(Renderer2D r, double x, double y, double w, double h,
float radiusTL, float radiusTR, float radiusBR, float radiusBL,
float softness,
int tintArgb,
float glassAlpha,
float blurAlpha,
float fresnelPower,
float fresnelAlpha,
float baseAlpha,
float fresnelMix,
float distortPx,
float squirclePower) {
if (w <= 0.0 || h <= 0.0) return;
r.effect(UiEffectSpec.liquidGlass(UiShape.roundedRect(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL),
softness, softness, distortPx, tintArgb));
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
if (fb == null) return;
GpuTextureView src = fb.getColorTextureView();
if (src == null) return;
GpuSampler sampler = PostProcessManager.getSampler();
if (sampler == null) return;
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreateBlur(UiBatchType.LIQUID_GLASS, src, sampler,
Renderer2D.DEFAULT_LIQUID_GLASS_BLUR_QUALITY, Renderer2D.LIQUID_GLASS_KAWASE_OFFSET_PX);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int a = (tintArgb >>> 24) & 0xFF;
int red = (tintArgb >>> 16) & 0xFF;
int green = (tintArgb >>> 8) & 0xFF;
int blue = tintArgb & 0xFF;
float clampedGlassAlpha = clamp01(glassAlpha);
float clampedBlurAlpha = clamp01(blurAlpha);
int finalA = (int) (a * clampedGlassAlpha);
float[] cornerRadiiTmp = r.cornerRadiiTmp;
normalizeCornerRadii(w, h, radiusTL, radiusTR, radiusBR, radiusBL, cornerRadiiTmp);
float thickness = Math.max(0.0f, softness);
float mix = clamp01(fresnelMix);
float fa = clamp01(fresnelAlpha);
float ba = clamp01(baseAlpha);
float packedDistort = packLiquidGlassPayload(distortPx, squirclePower, clampedBlurAlpha);
int i1 = mesh.vec2(x, y).raw2(mix, packedDistort).local2(x, y).color(red, green, blue, finalA)
.vec4(x, y, w, h)
.vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3])
.vec4(thickness, fresnelPower, fa, ba)
.next();
int i2 = mesh.vec2(x, y + h).raw2(mix, packedDistort).local2(x, y + h).color(red, green, blue, finalA)
.vec4(x, y, w, h)
.vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3])
.vec4(thickness, fresnelPower, fa, ba)
.next();
int i3 = mesh.vec2(x + w, y + h).raw2(mix, packedDistort).local2(x + w, y + h).color(red, green, blue, finalA)
.vec4(x, y, w, h)
.vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3])
.vec4(thickness, fresnelPower, fa, ba)
.next();
int i4 = mesh.vec2(x + w, y).raw2(mix, packedDistort).local2(x + w, y).color(red, green, blue, finalA)
.vec4(x, y, w, h)
.vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3])
.vec4(thickness, fresnelPower, fa, ba)
.next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
}

View file

@ -1,117 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.util.Mth;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeDegrees;
final class Renderer2DOrbiz {
private Renderer2DOrbiz() {
}
static void orbizRing(Renderer2D r, double cx, double cy,
double radius, double thickness,
float startAngleDeg, float sweepAngleDeg,
float softness, float glowRadius, float glowStrength,
int primaryArgb, int secondaryArgb,
int colorMode, float angularOffset, float angularCycles,
float trackAlpha, float bodyAlpha, float headBoost,
float trackGlow, boolean caps) {
if (r.textured) {
throw new IllegalStateException("Orbiz ring drawing is supported only on Renderer2D.COLOR.");
}
if (radius <= 0.0 || thickness <= 0.0) return;
if (((primaryArgb >>> 24) & 0xFF) <= 0 && ((secondaryArgb >>> 24) & 0xFF) <= 0) return;
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ORBIZ_RING, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int a = (primaryArgb >>> 24) & 0xFF;
int red = (primaryArgb >>> 16) & 0xFF;
int green = (primaryArgb >>> 8) & 0xFF;
int blue = primaryArgb & 0xFF;
float sr = ((secondaryArgb >>> 16) & 0xFF) / 255.0f;
float sg = ((secondaryArgb >>> 8) & 0xFF) / 255.0f;
float sb = (secondaryArgb & 0xFF) / 255.0f;
float sa = ((secondaryArgb >>> 24) & 0xFF) / 255.0f;
float stroke = (float) Math.max(0.0, thickness);
float ringRadius = (float) Math.max(0.0, radius);
float soft = Math.max(0.0f, softness);
float glow = Math.max(0.0f, glowRadius);
float expand = stroke * 0.5f + soft + glow + 1.0f;
double outerRadius = radius + expand;
double x = cx - outerRadius;
double y = cy - outerRadius;
double w = outerRadius * 2.0;
double h = outerRadius * 2.0;
double ringX = cx - radius;
double ringY = cy - radius;
double ringSize = radius * 2.0;
float start = normalizeDegrees(startAngleDeg);
float sweep = Mth.clamp(sweepAngleDeg, 0.0f, 360.0f);
float offset = angularOffset - (float) Math.floor(angularOffset);
float cycles = Math.max(0.001f, angularCycles);
float mode = Math.max(0, colorMode);
float track = Mth.clamp(trackAlpha, 0.0f, 1.0f);
float body = Mth.clamp(bodyAlpha, 0.0f, 1.0f);
float glowPower = Math.max(0.0f, glowStrength);
float head = Math.max(0.0f, headBoost);
float trackGlowAmount = Mth.clamp(trackGlow, 0.0f, 1.0f);
int i1 = mesh.vec2(x, y).local2(x, y).color(red, green, blue, a)
.vec4(ringX, ringY, ringSize, ringSize)
.vec4(ringRadius, soft, stroke, start)
.vec4(sweep, glow, glowPower, mode)
.vec4(sr, sg, sb, sa)
.vec4(offset, cycles, 0.0f, caps ? 1.0f : 0.0f)
.vec4(track, body, head, trackGlowAmount)
.next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(red, green, blue, a)
.vec4(ringX, ringY, ringSize, ringSize)
.vec4(ringRadius, soft, stroke, start)
.vec4(sweep, glow, glowPower, mode)
.vec4(sr, sg, sb, sa)
.vec4(offset, cycles, 0.0f, caps ? 1.0f : 0.0f)
.vec4(track, body, head, trackGlowAmount)
.next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a)
.vec4(ringX, ringY, ringSize, ringSize)
.vec4(ringRadius, soft, stroke, start)
.vec4(sweep, glow, glowPower, mode)
.vec4(sr, sg, sb, sa)
.vec4(offset, cycles, 0.0f, caps ? 1.0f : 0.0f)
.vec4(track, body, head, trackGlowAmount)
.next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a)
.vec4(ringX, ringY, ringSize, ringSize)
.vec4(ringRadius, soft, stroke, start)
.vec4(sweep, glow, glowPower, mode)
.vec4(sr, sg, sb, sa)
.vec4(offset, cycles, 0.0f, caps ? 1.0f : 0.0f)
.vec4(track, body, head, trackGlowAmount)
.next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
}

View file

@ -1,183 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import dev.loki.lovisual.render.engine.command.UiShapeCommand;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiBoxPathBuilder;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiBoxShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaintKind;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPathCap;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPathJoin;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.*;
final class Renderer2DPolygons {
private Renderer2DPolygons() {
}
static void box(Renderer2D r, UiBoxShape box, UiPaint paint, UiStroke stroke, boolean fill) {
if (box == null || paint == null) return;
UiStroke safeStroke = stroke == null ? UiStroke.NONE : stroke;
Renderer2D.recordUi(new UiShapeCommand(UiShape.box(box), paint, safeStroke, fill));
renderFlexibleBoxFallback(r, box, paint, safeStroke, fill);
}
static void renderFlexibleBoxFallback(Renderer2D r, UiBoxShape box, UiPaint paint, UiStroke stroke, boolean fill) {
if (r.textured || box == null || paint == null) return;
double[] rectShapeTmp = r.rectShapeTmp;
int count = UiBoxPathBuilder.write(box, rectShapeTmp, rectShapeTmp.length / 2);
if (count < 3) return;
if (fill) {
switch (paint.kind()) {
case LINEAR_GRADIENT ->
polygonGradient(r, rectShapeTmp, count, box.bounds().x(), box.bounds().y(), box.bounds().width(), box.bounds().height(),
paint.topLeft(), paint.topRight(), paint.angleDeg(), paint.offsetPx());
default -> polygon(r, rectShapeTmp, count, paint.solidColor());
}
} else if (stroke != null && stroke.enabled()) {
if (paint.kind() == UiPaintKind.LINEAR_GRADIENT) {
polylineLinearGradient(r, rectShapeTmp, count, stroke.thickness(), true,
box.bounds().x(), box.bounds().y(), box.bounds().width(), box.bounds().height(),
paint.topLeft(), paint.topRight(), paint.angleDeg(), paint.offsetPx(),
stroke.join() == UiPathJoin.ROUND || stroke.cap() == UiPathCap.ROUND);
} else {
polyline(r, rectShapeTmp, count, stroke.thickness(), true, paint.solidColor(),
stroke.join() == UiPathJoin.ROUND || stroke.cap() == UiPathCap.ROUND);
}
}
}
static void polygon(Renderer2D r, double[] points, int pointCount, int argb) {
if (r.textured) {
throw new IllegalStateException("Polygon drawing is supported only on Renderer2D.COLOR.");
}
int safeCount = Math.min(pointCount, points != null ? points.length / 2 : 0);
safeCount = Math.min(safeCount, Math.min(r.polygonIndexTmp.length, r.polygonVertexTmp.length));
if (points == null || safeCount < 3) return;
r.path(UiShape.polyline(points, safeCount, true), UiPaint.solid(argb), UiStroke.NONE, true);
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.COLORED, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
appendPolygon(mesh, points, safeCount, r.polygonIndexTmp, r.polygonVertexTmp, argb);
r.endAutoBatch(auto);
}
static void polygonGradient(Renderer2D r, double[] points, int pointCount,
double x, double y, double width, double height,
int startArgb, int endArgb, float angleDeg, float offsetPx) {
if (r.textured) {
throw new IllegalStateException("Polygon drawing is supported only on Renderer2D.COLOR.");
}
if (points == null || pointCount < 3) return;
r.path(UiShape.polyline(points, pointCount, true), UiPaint.linear(startArgb, endArgb, angleDeg, offsetPx), UiStroke.NONE, true);
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.COLORED, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
appendPolygonGradient(mesh, points, pointCount, r.polygonIndexTmp, r.polygonVertexTmp,
x, y, width, height, startArgb, endArgb, angleDeg, offsetPx);
r.endAutoBatch(auto);
}
static void polygonStroke(Renderer2D r, double[] points, int pointCount, double thickness, boolean closed, int argb) {
polyline(r, points, pointCount, thickness, closed, argb, false);
}
static void polyline(Renderer2D r, double[] points, int pointCount, double thickness, boolean closed, int argb) {
polyline(r, points, pointCount, thickness, closed, argb, false);
}
static void polyline(Renderer2D r, double[] points, int pointCount, double thickness, boolean closed, int argb, boolean roundCapsAndJoins) {
if (r.textured) {
throw new IllegalStateException("Polyline drawing is supported only on Renderer2D.COLOR.");
}
if (points == null || pointCount < 2) return;
double t = Math.max(0.0, thickness);
if (t <= 0.0) return;
r.path(UiShape.polyline(points, pointCount, closed), UiPaint.solid(argb),
roundCapsAndJoins ? UiStroke.of(t).withRoundCapsAndJoins() : UiStroke.of(t), false);
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.COLORED, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
appendPolyline(mesh, points, pointCount, t, closed, argb, roundCapsAndJoins);
r.endAutoBatch(auto);
}
static void polylineGradient(Renderer2D r, double[] points, int pointCount, double thickness, boolean closed,
int startArgb, int endArgb, boolean roundCapsAndJoins) {
if (r.textured) {
throw new IllegalStateException("Polyline drawing is supported only on Renderer2D.COLOR.");
}
if (points == null || pointCount < 2) return;
double t = Math.max(0.0, thickness);
if (t <= 0.0) return;
r.path(UiShape.polyline(points, pointCount, closed), UiPaint.corners(startArgb, endArgb, endArgb, startArgb),
roundCapsAndJoins ? UiStroke.of(t).withRoundCapsAndJoins() : UiStroke.of(t), false);
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.COLORED, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
appendPolylineGradient(mesh, points, pointCount, t, closed, startArgb, endArgb, roundCapsAndJoins);
r.endAutoBatch(auto);
}
static void polylineLinearGradient(Renderer2D r, double[] points, int pointCount, double thickness, boolean closed,
double x, double y, double width, double height,
int startArgb, int endArgb, float angleDeg, float offsetPx,
boolean roundCapsAndJoins) {
if (r.textured) {
throw new IllegalStateException("Polyline drawing is supported only on Renderer2D.COLOR.");
}
if (points == null || pointCount < 2) return;
double t = Math.max(0.0, thickness);
if (t <= 0.0) return;
r.path(UiShape.polyline(points, pointCount, closed), UiPaint.linear(startArgb, endArgb, angleDeg, offsetPx),
roundCapsAndJoins ? UiStroke.of(t).withRoundCapsAndJoins() : UiStroke.of(t), false);
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.COLORED, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
appendPolylineLinearGradient(mesh, points, pointCount, t, closed,
x, y, width, height, startArgb, endArgb, angleDeg, offsetPx, roundCapsAndJoins);
r.endAutoBatch(auto);
}
}

View file

@ -1,105 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.util.Mth;
final class Renderer2DSmokeFill {
private Renderer2DSmokeFill() {
}
static void roundedSmokeFill(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
float fillRatio,
boolean fromRight,
int firstArgb,
int secondArgb,
int thirdArgb,
float time,
float smokeScale,
float smokeMix,
int octaves,
float flowX,
float flowY,
float intensity) {
if (w <= 0.0 || h <= 0.0) return;
float fill = Mth.clamp(fillRatio, 0.0f, 1.0f);
if (fill <= 0.0001f) return;
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_FILL_SMOKE, null, null);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int a0 = (firstArgb >>> 24) & 0xFF;
int r0 = (firstArgb >>> 16) & 0xFF;
int g0 = (firstArgb >>> 8) & 0xFF;
int b0 = firstArgb & 0xFF;
float r1 = ((secondArgb >>> 16) & 0xFF) / 255.0f;
float g1 = ((secondArgb >>> 8) & 0xFF) / 255.0f;
float b1 = (secondArgb & 0xFF) / 255.0f;
float a1 = ((secondArgb >>> 24) & 0xFF) / 255.0f;
float r2 = ((thirdArgb >>> 16) & 0xFF) / 255.0f;
float g2 = ((thirdArgb >>> 8) & 0xFF) / 255.0f;
float b2 = (thirdArgb & 0xFF) / 255.0f;
float a2 = ((thirdArgb >>> 24) & 0xFF) / 255.0f;
float fromRightFlag = fromRight ? 1.0f : 0.0f;
float safeScale = Math.max(0.1f, smokeScale);
float safeMix = Mth.clamp(smokeMix, 0.0f, 1.0f);
float safeOctaves = Mth.clamp(octaves, 1, 8);
float safeIntensity = Math.max(0.0f, intensity);
int i1 = mesh.vec2(x, y).local2(x, y).color(r0, g0, b0, a0)
.vec4((float) x, (float) y, (float) w, (float) h)
.vec4(radius, softness, fill, fromRightFlag)
.vec4(r1, g1, b1, a1)
.vec4(r2, g2, b2, a2)
.vec4(time, safeScale, safeMix, safeOctaves)
.vec4(flowX, flowY, safeIntensity, 0.0f)
.next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(r0, g0, b0, a0)
.vec4((float) x, (float) y, (float) w, (float) h)
.vec4(radius, softness, fill, fromRightFlag)
.vec4(r1, g1, b1, a1)
.vec4(r2, g2, b2, a2)
.vec4(time, safeScale, safeMix, safeOctaves)
.vec4(flowX, flowY, safeIntensity, 0.0f)
.next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(r0, g0, b0, a0)
.vec4((float) x, (float) y, (float) w, (float) h)
.vec4(radius, softness, fill, fromRightFlag)
.vec4(r1, g1, b1, a1)
.vec4(r2, g2, b2, a2)
.vec4(time, safeScale, safeMix, safeOctaves)
.vec4(flowX, flowY, safeIntensity, 0.0f)
.next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(r0, g0, b0, a0)
.vec4((float) x, (float) y, (float) w, (float) h)
.vec4(radius, softness, fill, fromRightFlag)
.vec4(r1, g1, b1, a1)
.vec4(r2, g2, b2, a2)
.vec4(time, safeScale, safeMix, safeOctaves)
.vec4(flowX, flowY, safeIntensity, 0.0f)
.next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
}

View file

@ -1,185 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.command.UiShapeCommand;
import dev.loki.lovisual.render.engine.command.UiTextureDrawCommand;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampRoundedRadius;
final class Renderer2DTextures {
private Renderer2DTextures() {
}
static void textureQuad(Renderer2D r, GpuTextureView samplerView, GpuSampler sampler,
double x, double y, double w, double h, int argb) {
if (r.textured) {
throw new IllegalStateException("Batched texture quad drawing is supported only on Renderer2D.COLOR.");
}
if (samplerView == null || sampler == null) return;
if (w == 0.0 || h == 0.0) return;
Renderer2D.recordUi(new UiTextureDrawCommand(samplerView, sampler, UiShape.rect(x, y, w, h), UiPaint.solid(argb), 0f, 0f, 1f, 1f, false));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.TEXTURED, samplerView, sampler);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF;
int i1 = mesh.vec2(x, y).raw2(0.0, 0.0).color(red, green, blue, a).next();
int i2 = mesh.vec2(x, y + h).raw2(0.0, 1.0).color(red, green, blue, a).next();
int i3 = mesh.vec2(x + w, y + h).raw2(1.0, 1.0).color(red, green, blue, a).next();
int i4 = mesh.vec2(x + w, y).raw2(1.0, 0.0).color(red, green, blue, a).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
static void msdfTextureQuad(Renderer2D r, GpuTextureView samplerView, GpuSampler sampler,
double x, double y, double w, double h,
int topLeft, int topRight, int bottomRight, int bottomLeft,
float pxRange, int atlasWidth, int atlasHeight) {
if (r.textured) {
throw new IllegalStateException("Batched MSDF texture quad drawing is supported only on Renderer2D.COLOR.");
}
if (samplerView == null || sampler == null) return;
if (w == 0.0 || h == 0.0) return;
if (pxRange <= 0.0f || atlasWidth <= 0 || atlasHeight <= 0) return;
Renderer2D.recordUi(new UiTextureDrawCommand(samplerView, sampler, UiShape.rect(x, y, w, h), UiPaint.solid(topLeft), 0f, 0f, 1f, 1f, false));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreateMsdf(samplerView, sampler, pxRange, atlasWidth, atlasHeight);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int i1 = mesh.vec2(x, y).raw2(0.0, 0.0).color((topLeft >>> 16) & 0xFF, (topLeft >>> 8) & 0xFF, topLeft & 0xFF, (topLeft >>> 24) & 0xFF).next();
int i2 = mesh.vec2(x, y + h).raw2(0.0, 1.0).color((bottomLeft >>> 16) & 0xFF, (bottomLeft >>> 8) & 0xFF, bottomLeft & 0xFF, (bottomLeft >>> 24) & 0xFF).next();
int i3 = mesh.vec2(x + w, y + h).raw2(1.0, 1.0).color((bottomRight >>> 16) & 0xFF, (bottomRight >>> 8) & 0xFF, bottomRight & 0xFF, (bottomRight >>> 24) & 0xFF).next();
int i4 = mesh.vec2(x + w, y).raw2(1.0, 0.0).color((topRight >>> 16) & 0xFF, (topRight >>> 8) & 0xFF, topRight & 0xFF, (topRight >>> 24) & 0xFF).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
static void texQuad(Renderer2D r, double x, double y, double width, double height,
double texX1, double texY1, double texX2, double texY2,
int argb) {
Renderer2D.recordUi(new UiShapeCommand(UiShape.rect(x, y, width, height), UiPaint.solid(argb), UiStroke.NONE, true));
MeshBuilder texturedTriangles = r.texturedTriangles;
if (texturedTriangles == null) return;
texturedTriangles.alpha = r.alpha;
texturedTriangles.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF;
int i1 = texturedTriangles.vec2(x, y).raw2(texX1, texY1).color(red, green, blue, a).next();
int i2 = texturedTriangles.vec2(x, y + height).raw2(texX1, texY2).color(red, green, blue, a).next();
int i3 = texturedTriangles.vec2(x + width, y + height).raw2(texX2, texY2).color(red, green, blue, a).next();
int i4 = texturedTriangles.vec2(x + width, y).raw2(texX2, texY1).color(red, green, blue, a).next();
texturedTriangles.quad(i1, i2, i3, i4);
}
static void roundedTexRect(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
double texX1, double texY1, double texX2, double texY2,
int argb, GpuTextureView samplerView, GpuSampler sampler) {
if (!r.textured) {
throw new IllegalStateException("Renderer2D is not configured for textures.");
}
if (samplerView == null || sampler == null) return;
Renderer2D.recordUi(new UiTextureDrawCommand(samplerView, sampler, UiShape.roundedRect(x, y, w, h, radius), UiPaint.solid(argb),
(float) texX1, (float) texY1, (float) texX2, (float) texY2, false));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_TEXTURED, samplerView, sampler);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF;
float clampedRadius = clampRoundedRadius(radius, w, h);
int i1 = mesh.vec2(x, y).raw2(texX1, texY1).local2(x, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i2 = mesh.vec2(x, y + h).raw2(texX1, texY2).local2(x, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).raw2(texX2, texY2).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).raw2(texX2, texY1).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
static void roundedTexMaskRect(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
double texX1, double texY1, double texX2, double texY2,
int argb, GpuTextureView samplerView, GpuSampler sampler) {
if (!r.textured) {
throw new IllegalStateException("Renderer2D is not configured for textures.");
}
if (samplerView == null || sampler == null) return;
Renderer2D.recordUi(new UiTextureDrawCommand(samplerView, sampler, UiShape.roundedRect(x, y, w, h, radius), UiPaint.solid(argb),
(float) texX1, (float) texY1, (float) texX2, (float) texY2, true));
boolean auto = r.beginAutoBatch();
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_TEXTURED_MASK, samplerView, sampler);
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF;
float clampedRadius = clampRoundedRadius(radius, w, h);
int i1 = mesh.vec2(x, y).raw2(texX1, texY1).local2(x, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i2 = mesh.vec2(x, y + h).raw2(texX1, texY2).local2(x, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).raw2(texX2, texY2).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).raw2(texX2, texY1).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4);
r.endAutoBatch(auto);
}
}

View file

@ -0,0 +1,104 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.UiRenderDispatcher;
/**
* Shared batch scope and scratch buffers for the Renderer2D helper backends.
*
* <p>{@link #open} starts an auto batch (only if none is active yet) and fetches the
* batch for the requested type; {@link #close} flushes only when this call chain
* actually started the batch. Nesting is handled with a depth counter, so helpers
* may call other drawing helpers freely.</p>
*/
public final class Batch2D {
/** Render-thread scratch buffers shared by the helper backends. */
public static final float[] CORNER_RADII = new float[4];
public static final double[] RECT_SHAPE = new double[256];
public static final double[] CONNECTOR = new double[64];
public static final double[] CONNECTOR_ANCHOR = new double[4];
public static final int[] POLY_INDEX = new int[64];
public static final int[] POLY_VERTEX = new int[64];
public static final double[] PROGRESS_SHAPE = new double[256];
public static final int[] GRADIENT = new int[4];
private static final int[] RGBA = new int[4];
private static final float[] NORM = new float[4];
private static int depth;
private static boolean rootAuto;
private Batch2D() {
}
public static DrawBatch open(Renderer2D r, UiBatchType type) {
return open(r, type, null, null);
}
public static DrawBatch open(Renderer2D r, UiBatchType type, GpuTextureView view, GpuSampler sampler) {
beginScope();
return finish(r, Renderer2D.UI_BATCHER.getOrCreate(type, view, sampler));
}
public static DrawBatch openBlur(Renderer2D r, UiBatchType type, GpuTextureView view, GpuSampler sampler,
Renderer2D.BlurQuality quality, float offsetPx) {
beginScope();
return finish(r, Renderer2D.UI_BATCHER.getOrCreateBlur(type, view, sampler, quality, offsetPx));
}
public static DrawBatch openMsdf(Renderer2D r, GpuTextureView view, GpuSampler sampler,
float pxRange, int atlasWidth, int atlasHeight) {
beginScope();
return finish(r, Renderer2D.UI_BATCHER.getOrCreateMsdf(view, sampler, pxRange, atlasWidth, atlasHeight));
}
public static void close() {
if (depth > 0 && --depth == 0 && rootAuto) {
rootAuto = false;
UiRenderDispatcher.endAutoBatch(true);
}
}
private static void beginScope() {
if (depth == 0) rootAuto = UiRenderDispatcher.beginAutoBatch();
depth++;
}
private static DrawBatch finish(Renderer2D r, DrawBatch batch) {
if (batch == null) {
close();
return null;
}
batch.mesh.alpha = r.getAlpha();
return batch;
}
/** Unpacks {@code argb} into the shared scratch as {r, g, b, a}. */
public static int[] rgba(int argb) {
RGBA[0] = (argb >>> 16) & 0xFF;
RGBA[1] = (argb >>> 8) & 0xFF;
RGBA[2] = argb & 0xFF;
RGBA[3] = (argb >>> 24) & 0xFF;
return RGBA;
}
/** Unpacks {@code argb} into normalized floats {r, g, b, a} in range 0..1. */
public static float[] rgbaNorm(int argb) {
NORM[0] = ((argb >>> 16) & 0xFF) / 255.0f;
NORM[1] = ((argb >>> 8) & 0xFF) / 255.0f;
NORM[2] = (argb & 0xFF) / 255.0f;
NORM[3] = ((argb >>> 24) & 0xFF) / 255.0f;
return NORM;
}
}

View file

@ -0,0 +1,170 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.renderer2d.path.Polygons2D;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampChamferAxis;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.computeLinearGradientColors;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeChamfers;
/** Chamfered (cut-corner) rects and beveled 3D-style plates. */
public final class Chamfered2D {
private Chamfered2D() {
}
public static void chamferedRectQuadAxes(Renderer2D r, double x, double y, double w, double h,
double chamferTLX, double chamferTLY,
double chamferTRX, double chamferTRY,
double chamferBRX, double chamferBRY,
double chamferBLX, double chamferBLY,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
r.shape(UiShape.chamferedRectAxes(x, y, w, h, chamferTLX, chamferTLY, chamferTRX, chamferTRY,
chamferBRX, chamferBRY, chamferBLX, chamferBLY),
UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft));
DrawBatch batch = Batch2D.open(r, UiBatchType.CHAMFERED);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int tlA = (cTopLeft >>> 24) & 0xFF;
int tlR = (cTopLeft >>> 16) & 0xFF;
int tlG = (cTopLeft >>> 8) & 0xFF;
int tlB = cTopLeft & 0xFF;
int trA = (cTopRight >>> 24) & 0xFF;
int trR = (cTopRight >>> 16) & 0xFF;
int trG = (cTopRight >>> 8) & 0xFF;
int trB = cTopRight & 0xFF;
int brA = (cBottomRight >>> 24) & 0xFF;
int brR = (cBottomRight >>> 16) & 0xFF;
int brG = (cBottomRight >>> 8) & 0xFF;
int brB = cBottomRight & 0xFF;
int blA = (cBottomLeft >>> 24) & 0xFF;
int blR = (cBottomLeft >>> 16) & 0xFF;
int blG = (cBottomLeft >>> 8) & 0xFF;
int blB = cBottomLeft & 0xFF;
float tlX = clampChamferAxis(chamferTLX, w);
float trX = clampChamferAxis(chamferTRX, w);
float brX = clampChamferAxis(chamferBRX, w);
float blX = clampChamferAxis(chamferBLX, w);
float tlY = clampChamferAxis(chamferTLY, h);
float trY = clampChamferAxis(chamferTRY, h);
float brY = clampChamferAxis(chamferBRY, h);
float blY = clampChamferAxis(chamferBLY, h);
int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(x, y, w, h).vec4(tlX, trX, brX, blX).vec4(tlY, trY, brY, blY).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(x, y, w, h).vec4(tlX, trX, brX, blX).vec4(tlY, trY, brY, blY).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h).vec4(tlX, trX, brX, blX).vec4(tlY, trY, brY, blY).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h).vec4(tlX, trX, brX, blX).vec4(tlY, trY, brY, blY).next();
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void chamferedRectQuad(Renderer2D r, double x, double y, double w, double h,
double chamferTL, double chamferTR, double chamferBR, double chamferBL,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
float[] c = Batch2D.CORNER_RADII;
normalizeChamfers(w, h, (float) chamferTL, (float) chamferTR, (float) chamferBR, (float) chamferBL, c);
chamferedRectQuadAxes(r, x, y, w, h,
c[0], c[0], c[1], c[1], c[2], c[2], c[3], c[3],
cTopLeft, cTopRight, cBottomRight, cBottomLeft);
}
public static void chamferedRectGradient(Renderer2D r, double x, double y, double width, double height,
double chamferTL, double chamferTR, double chamferBR, double chamferBL,
int startArgb, int endArgb, float angleDeg, float offsetPx) {
int[] g = Batch2D.GRADIENT;
computeLinearGradientColors((float) width, (float) height, startArgb, endArgb, angleDeg, offsetPx, g);
chamferedRectQuad(r, x, y, width, height, chamferTL, chamferTR, chamferBR, chamferBL,
g[0], g[1], g[2], g[3]);
}
public static void chamferedRectStrokeQuad(Renderer2D r, double x, double y, double w, double h,
double chamferTL, double chamferTR, double chamferBR, double chamferBL,
double thickness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
r.shapeStroke(UiShape.chamferedRect(x, y, w, h, chamferTL, chamferTR, chamferBR, chamferBL),
UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft), UiStroke.of(thickness));
DrawBatch batch = Batch2D.open(r, UiBatchType.CHAMFERED_STROKE);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int tlA = (cTopLeft >>> 24) & 0xFF;
int tlR = (cTopLeft >>> 16) & 0xFF;
int tlG = (cTopLeft >>> 8) & 0xFF;
int tlB = cTopLeft & 0xFF;
int trA = (cTopRight >>> 24) & 0xFF;
int trR = (cTopRight >>> 16) & 0xFF;
int trG = (cTopRight >>> 8) & 0xFF;
int trB = cTopRight & 0xFF;
int brA = (cBottomRight >>> 24) & 0xFF;
int brR = (cBottomRight >>> 16) & 0xFF;
int brG = (cBottomRight >>> 8) & 0xFF;
int brB = cBottomRight & 0xFF;
int blA = (cBottomLeft >>> 24) & 0xFF;
int blR = (cBottomLeft >>> 16) & 0xFF;
int blG = (cBottomLeft >>> 8) & 0xFF;
int blB = cBottomLeft & 0xFF;
float[] c = Batch2D.CORNER_RADII;
normalizeChamfers(w, h, (float) chamferTL, (float) chamferTR, (float) chamferBR, (float) chamferBL, c);
float stroke = (float) Math.max(0.0, thickness);
int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(x, y, w, h).vec4(c[0], c[1], c[2], c[3]).vec4(stroke, 0f, 0f, 0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(x, y, w, h).vec4(c[0], c[1], c[2], c[3]).vec4(stroke, 0f, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h).vec4(c[0], c[1], c[2], c[3]).vec4(stroke, 0f, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h).vec4(c[0], c[1], c[2], c[3]).vec4(stroke, 0f, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void beveledRect(Renderer2D r, double x, double y, double width, double height,
double bevel, int fillArgb, int highlightArgb, int shadowArgb) {
chamferedRectQuad(r, x, y, width, height, bevel, bevel, bevel, bevel,
fillArgb, fillArgb, fillArgb, fillArgb);
bevelEdges(r, x, y, width, height, bevel, highlightArgb, shadowArgb);
}
public static void beveledRectGradient(Renderer2D r, double x, double y, double width, double height,
double bevel, int startArgb, int endArgb,
int highlightArgb, int shadowArgb, float angleDeg, float offsetPx) {
chamferedRectGradient(r, x, y, width, height, bevel, bevel, bevel, bevel,
startArgb, endArgb, angleDeg, offsetPx);
bevelEdges(r, x, y, width, height, bevel, highlightArgb, shadowArgb);
}
private static void bevelEdges(Renderer2D r, double x, double y, double width, double height,
double bevel, int highlightArgb, int shadowArgb) {
double t = Math.max(1.0, Math.min(Math.min(Math.abs(width), Math.abs(height)) * 0.25, bevel));
double[] c = Batch2D.CONNECTOR;
c[0] = x + bevel;
c[1] = y;
c[2] = x + width - bevel;
c[3] = y;
c[4] = x + width;
c[5] = y + bevel;
Polygons2D.polyline(r, c, 3, t, false, highlightArgb, false);
c[0] = x + width;
c[1] = y + height - bevel;
c[2] = x + width - bevel;
c[3] = y + height;
c[4] = x + bevel;
c[5] = y + height;
c[6] = x;
c[7] = y + height - bevel;
Polygons2D.polyline(r, c, 4, t, false, shadowArgb, false);
}
}

View file

@ -5,8 +5,9 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.render.engine.renderer; package dev.loki.lovisual.render.engine.renderer.renderer2d;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch; import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType; import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint; import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
@ -17,67 +18,49 @@ import java.util.function.Consumer;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.appendColoredQuad; import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.appendColoredQuad;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampRoundedRadius; import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampRoundedRadius;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.computeLinearGradientColors;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeCornerRadii; import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeCornerRadii;
final class Renderer2DRects { /** Filled rect family: solid quads, rounded rects, per-corner radii and gradients. */
private Renderer2DRects() { public final class Rects2D {
private Rects2D() {
} }
static void quadBatch(Renderer2D r, Consumer<Renderer2D.QuadBatch> consumer) { public static void quadBatch(Renderer2D r, Consumer<Renderer2D.QuadBatch> consumer) {
if (r.textured) { if (r.isTextured()) {
throw new IllegalStateException("Quad batching is supported only on Renderer2D.COLOR."); throw new IllegalStateException("Quad batching is supported only on Renderer2D.COLOR.");
} }
if (consumer == null) return; if (consumer == null) return;
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.COLORED);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.COLORED, null, null); if (batch == null) return;
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
Renderer2D.QuadBatch quadBatch = new Renderer2D.QuadBatch(mesh);
try { try {
consumer.accept(quadBatch); consumer.accept(new Renderer2D.QuadBatch(batch.mesh));
} finally { } finally {
r.endAutoBatch(auto); Batch2D.close();
} }
} }
static void quadCorners(Renderer2D r, double x, double y, double width, double height, public static void quadCorners(Renderer2D r, double x, double y, double width, double height,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
r.shape(UiShape.rect(x, y, width, height), UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft)); r.shape(UiShape.rect(x, y, width, height), UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft));
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.COLORED);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.COLORED, null, null); if (batch == null) return;
if (batch == null) { appendColoredQuad(batch.mesh, x, y, width, height, cTopLeft, cTopRight, cBottomRight, cBottomLeft);
r.endAutoBatch(auto); Batch2D.close();
return;
}
MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
appendColoredQuad(mesh, x, y, width, height, cTopLeft, cTopRight, cBottomRight, cBottomLeft);
r.endAutoBatch(auto);
} }
static void roundedRectMaskedQuad(Renderer2D r, double x, double y, double w, double h, public static void roundedRectMaskedQuad(Renderer2D r, double x, double y, double w, double h,
double maskX, double maskY, double maskW, double maskH, double maskX, double maskY, double maskW, double maskH,
float radius, float softness, float radius, float softness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
r.shape(UiShape.roundedRect(maskX, maskY, maskW, maskH, radius), UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft)); r.shape(UiShape.roundedRect(maskX, maskY, maskW, maskH, radius),
boolean auto = r.beginAutoBatch(); UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft));
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED, null, null); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED);
if (batch == null) { if (batch == null) return;
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
float clampedRadius = clampRoundedRadius(radius, maskW, maskH);
int tlA = (cTopLeft >>> 24) & 0xFF; int tlA = (cTopLeft >>> 24) & 0xFF;
int tlR = (cTopLeft >>> 16) & 0xFF; int tlR = (cTopLeft >>> 16) & 0xFF;
@ -95,32 +78,27 @@ final class Renderer2DRects {
int blR = (cBottomLeft >>> 16) & 0xFF; int blR = (cBottomLeft >>> 16) & 0xFF;
int blG = (cBottomLeft >>> 8) & 0xFF; int blG = (cBottomLeft >>> 8) & 0xFF;
int blB = cBottomLeft & 0xFF; int blB = cBottomLeft & 0xFF;
float clampedRadius = clampRoundedRadius(radius, maskW, maskH);
int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next(); int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next(); int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next(); int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next(); int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
static void roundedRectCornersQuad(Renderer2D r, double x, double y, double w, double h, public static void roundedRectCornersQuad(Renderer2D r, double x, double y, double w, double h,
float radiusTL, float radiusTR, float radiusBR, float radiusBL, float radiusTL, float radiusTR, float radiusBR, float radiusBL,
float softness, float softness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
r.shape(UiShape.roundedRect(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL), UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft)); r.shape(UiShape.roundedRect(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL),
boolean auto = r.beginAutoBatch(); UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft));
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_CORNERS, null, null); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_CORNERS);
if (batch == null) { if (batch == null) return;
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
float[] radii = Batch2D.CORNER_RADII;
normalizeCornerRadii(w, h, radiusTL, radiusTR, radiusBR, radiusBL, radii);
int tlA = (cTopLeft >>> 24) & 0xFF; int tlA = (cTopLeft >>> 24) & 0xFF;
int tlR = (cTopLeft >>> 16) & 0xFF; int tlR = (cTopLeft >>> 16) & 0xFF;
@ -138,15 +116,31 @@ final class Renderer2DRects {
int blR = (cBottomLeft >>> 16) & 0xFF; int blR = (cBottomLeft >>> 16) & 0xFF;
int blG = (cBottomLeft >>> 8) & 0xFF; int blG = (cBottomLeft >>> 8) & 0xFF;
int blB = cBottomLeft & 0xFF; int blB = cBottomLeft & 0xFF;
float[] cornerRadiiTmp = r.cornerRadiiTmp;
normalizeCornerRadii(w, h, radiusTL, radiusTR, radiusBR, radiusBL, cornerRadiiTmp);
int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(0.0f, 0f, 0f, 0f).next(); int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(x, y, w, h)
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(0.0f, 0f, 0f, 0f).next(); .vec4(radii[0], radii[1], radii[2], radii[3]).vec4(0.0f, 0f, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(0.0f, 0f, 0f, 0f).next(); int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(x, y, w, h)
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(0.0f, 0f, 0f, 0f).next(); .vec4(radii[0], radii[1], radii[2], radii[3]).vec4(0.0f, 0f, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h)
.vec4(radii[0], radii[1], radii[2], radii[3]).vec4(0.0f, 0f, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h)
.vec4(radii[0], radii[1], radii[2], radii[3]).vec4(0.0f, 0f, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
}
public static void roundedRectGradientQuad(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
roundedRectMaskedQuad(r, x, y, w, h, x, y, w, h, radius, 0.0f,
cTopLeft, cTopRight, cBottomRight, cBottomLeft);
}
public static void roundedRectGradient(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
int startArgb, int endArgb, float angleDeg, float offsetPx) {
int[] g = Batch2D.GRADIENT;
computeLinearGradientColors((float) w, (float) h, startArgb, endArgb, angleDeg, offsetPx, g);
roundedRectGradientQuad(r, x, y, w, h, radius, 0.0f, g[0], g[1], g[2], g[3]);
}
}

View file

@ -5,10 +5,12 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.render.engine.renderer; package dev.loki.lovisual.render.engine.renderer.renderer2d;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch; import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType; import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.renderer2d.path.Polygons2D;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint; import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape; import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke; import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
@ -19,22 +21,17 @@ import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.bu
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampRoundedRadius; import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampRoundedRadius;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeCornerRadii; import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeCornerRadii;
final class Renderer2DRectsExtra { /** Stroked rounded rect family plus the clipped progress bar fill. */
private Renderer2DRectsExtra() { public final class Strokes2D {
private Strokes2D() {
} }
static void roundedRectStroke(Renderer2D r, double x, double y, double w, double h, public static void roundedRectStroke(Renderer2D r, double x, double y, double w, double h,
float radius, float softness, float thickness, int argb) { float radius, float softness, float thickness, int argb) {
r.shapeStroke(UiShape.roundedRect(x, y, w, h, radius), UiPaint.solid(argb), UiStroke.of(thickness)); r.shapeStroke(UiShape.roundedRect(x, y, w, h, radius), UiPaint.solid(argb), UiStroke.of(thickness));
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_STROKE);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_STROKE, null, null); if (batch == null) return;
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF; int a = (argb >>> 24) & 0xFF;
@ -49,91 +46,68 @@ final class Renderer2DRectsExtra {
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, 0f).next(); int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, 0f).next(); int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
static void roundedRectStrokeAngularGradient(Renderer2D r, double x, double y, double w, double h, public static void roundedRectStrokeAngularGradient(Renderer2D r, double x, double y, double w, double h,
float radius, float softness, float thickness, float radius, float softness, float thickness,
int startArgb, int endArgb, float offset) { int startArgb, int endArgb, float offset) {
r.shapeStroke(UiShape.roundedRect(x, y, w, h, radius), UiPaint.solid(startArgb), UiStroke.of(thickness)); r.shapeStroke(UiShape.roundedRect(x, y, w, h, radius), UiPaint.solid(startArgb), UiStroke.of(thickness));
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_STROKE_ANGULAR);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_STROKE_ANGULAR, null, null); if (batch == null) return;
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
int sa = (startArgb >>> 24) & 0xFF; int sa = (startArgb >>> 24) & 0xFF;
int sr = (startArgb >>> 16) & 0xFF; int sr = (startArgb >>> 16) & 0xFF;
int sg = (startArgb >>> 8) & 0xFF; int sg = (startArgb >>> 8) & 0xFF;
int sb = startArgb & 0xFF; int sb = startArgb & 0xFF;
float ea = ((endArgb >>> 24) & 0xFF) / 255.0f; float[] end = Batch2D.rgbaNorm(endArgb);
float er = ((endArgb >>> 16) & 0xFF) / 255.0f;
float eg = ((endArgb >>> 8) & 0xFF) / 255.0f;
float eb = (endArgb & 0xFF) / 255.0f;
float clampedRadius = clampRoundedRadius(radius, w, h); float clampedRadius = clampRoundedRadius(radius, w, h);
float stroke = Math.max(0.0f, thickness); float stroke = Math.max(0.0f, thickness);
float phase = offset - (float) Math.floor(offset); float phase = offset - (float) Math.floor(offset);
int i1 = mesh.vec2(x, y).local2(x, y).color(sr, sg, sb, sa).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, phase).vec4(er, eg, eb, ea).next(); int i1 = mesh.vec2(x, y).local2(x, y).color(sr, sg, sb, sa).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, phase).vec4(end[0], end[1], end[2], end[3]).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(sr, sg, sb, sa).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, phase).vec4(er, eg, eb, ea).next(); int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(sr, sg, sb, sa).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, phase).vec4(end[0], end[1], end[2], end[3]).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(sr, sg, sb, sa).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, phase).vec4(er, eg, eb, ea).next(); int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(sr, sg, sb, sa).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, phase).vec4(end[0], end[1], end[2], end[3]).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(sr, sg, sb, sa).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, phase).vec4(er, eg, eb, ea).next(); int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(sr, sg, sb, sa).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, phase).vec4(end[0], end[1], end[2], end[3]).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
static void roundedRectStrokeCorners(Renderer2D r, double x, double y, double w, double h, public static void roundedRectStrokeCorners(Renderer2D r, double x, double y, double w, double h,
float radiusTL, float radiusTR, float radiusBR, float radiusBL, float radiusTL, float radiusTR, float radiusBR, float radiusBL,
float softness, float thickness, int argb) { float softness, float thickness, int argb) {
r.shapeStroke(UiShape.roundedRect(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL), UiPaint.solid(argb), UiStroke.of(thickness)); r.shapeStroke(UiShape.roundedRect(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL), UiPaint.solid(argb), UiStroke.of(thickness));
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_STROKE_CORNERS);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_STROKE_CORNERS, null, null); if (batch == null) return;
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF; int a = (argb >>> 24) & 0xFF;
int red = (argb >>> 16) & 0xFF; int red = (argb >>> 16) & 0xFF;
int green = (argb >>> 8) & 0xFF; int green = (argb >>> 8) & 0xFF;
int blue = argb & 0xFF; int blue = argb & 0xFF;
float[] cornerRadiiTmp = r.cornerRadiiTmp; float[] radii = Batch2D.CORNER_RADII;
normalizeCornerRadii(w, h, radiusTL, radiusTR, radiusBR, radiusBL, cornerRadiiTmp); normalizeCornerRadii(w, h, radiusTL, radiusTR, radiusBR, radiusBL, radii);
float stroke = Math.max(0.0f, thickness); float stroke = Math.max(0.0f, thickness);
int i1 = mesh.vec2(x, y).local2(x, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(0.0f, stroke, 0f, 0f).next(); int i1 = mesh.vec2(x, y).local2(x, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(radii[0], radii[1], radii[2], radii[3]).vec4(0.0f, stroke, 0f, 0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(0.0f, stroke, 0f, 0f).next(); int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(radii[0], radii[1], radii[2], radii[3]).vec4(0.0f, stroke, 0f, 0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(0.0f, stroke, 0f, 0f).next(); int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(radii[0], radii[1], radii[2], radii[3]).vec4(0.0f, stroke, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(cornerRadiiTmp[0], cornerRadiiTmp[1], cornerRadiiTmp[2], cornerRadiiTmp[3]).vec4(0.0f, stroke, 0f, 0f).next(); int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(radii[0], radii[1], radii[2], radii[3]).vec4(0.0f, stroke, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
static void roundedRectStrokeGradientQuad(Renderer2D r, double x, double y, double w, double h, public static void roundedRectStrokeGradientQuad(Renderer2D r, double x, double y, double w, double h,
float radius, float softness, float thickness, float radius, float softness, float thickness,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
r.shapeStroke(UiShape.roundedRect(x, y, w, h, radius), UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft), UiStroke.of(thickness)); r.shapeStroke(UiShape.roundedRect(x, y, w, h, radius),
boolean auto = r.beginAutoBatch(); UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft), UiStroke.of(thickness));
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_STROKE, null, null); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_STROKE);
if (batch == null) { if (batch == null) return;
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
int tlA = (cTopLeft >>> 24) & 0xFF; int tlA = (cTopLeft >>> 24) & 0xFF;
@ -160,29 +134,35 @@ final class Renderer2DRectsExtra {
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, 0f).next(); int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, 0f).next(); int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, stroke, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
static void roundedProgressRectGradient(Renderer2D r, double x, double y, double w, double h, /**
float radius, float softness, * Draws a progress segment clipped by the full rounded bar outline.
float progress, *
boolean fromRight, * <p>Unlike drawing a rounded rectangle with the current fill width, this keeps the moving
int startArgb, int endArgb, * progress edge straight and lets only the fixed outer bar bounds define rounded ends. That
float angleDeg, float offsetPx) { * prevents small health values from becoming a pill/capsule and prevents the fill from leaking
* outside the left/right rounded shell when the progress edge crosses an outer corner.</p>
*/
public static void roundedProgressRectGradient(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
float progress, boolean fromRight,
int startArgb, int endArgb,
float angleDeg, float offsetPx) {
if (w <= 0.0 || h <= 0.0) return; if (w <= 0.0 || h <= 0.0) return;
float p = Mth.clamp(progress, 0.0f, 1.0f); float p = Mth.clamp(progress, 0.0f, 1.0f);
if (p <= 0.0001f) return; if (p <= 0.0001f) return;
if (p >= 0.9999f) { if (p >= 0.9999f) {
r.roundedRectGradient(x, y, w, h, radius, softness, startArgb, endArgb, angleDeg, offsetPx); Rects2D.roundedRectGradient(r, x, y, w, h, radius, softness, startArgb, endArgb, angleDeg, offsetPx);
return; return;
} }
double fillW = Math.max(0.0, Math.min(w, w * p)); double fillW = Math.max(0.0, Math.min(w, w * p));
double fillX = fromRight ? x + w - fillW : x; double fillX = fromRight ? x + w - fillW : x;
double clipX = fromRight ? fillX : x + fillW; double clipX = fromRight ? fillX : x + fillW;
int count = buildRoundedProgressRect(r.progressShapeTmp, r.rectShapeTmp, x, y, w, h, radius, clipX, fromRight); int count = buildRoundedProgressRect(Batch2D.PROGRESS_SHAPE, Batch2D.RECT_SHAPE, x, y, w, h, radius, clipX, fromRight);
if (count < 3) return; if (count < 3) return;
Renderer2DPolygons.polygonGradient(r, r.progressShapeTmp, count, fillX, y, fillW, h, startArgb, endArgb, angleDeg, offsetPx); Polygons2D.polygonGradient(r, Batch2D.PROGRESS_SHAPE, count, fillX, y, fillW, h, startArgb, endArgb, angleDeg, offsetPx);
} }
} }

View file

@ -0,0 +1,151 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.fx;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.postprocess.PostProcessManager;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.ui.BlurSource;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiEffectSpec;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
import java.util.function.Consumer;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clamp01;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampChamfer;
/** Background blur rectangles fed from the main framebuffer. */
public final class Blur2D {
private Blur2D() {
}
private static Renderer2D.BlurQuality legacyBlurQuality(float legacyQuality) {
if (legacyQuality >= 32.0f) return Renderer2D.BlurQuality.ULTRA;
if (legacyQuality >= 12.0f) return Renderer2D.BlurQuality.HIGH;
if (legacyQuality >= 5.0f) return Renderer2D.BlurQuality.MEDIUM;
return Renderer2D.BlurQuality.LOW;
}
private static float legacyKawaseOffset(float legacyQuality) {
if (!Float.isFinite(legacyQuality)) return Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
return Mth.clamp(legacyQuality / 18.0f, 0.85f, 3.25f);
}
/** Main framebuffer color texture plus the shared post-process sampler. */
public static BlurSource source() {
Minecraft mc = Minecraft.getInstance();
if (mc == null) return null;
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
if (fb == null) return null;
GpuTextureView src = fb.getColorTextureView();
if (src == null) return null;
GpuSampler sampler = PostProcessManager.getSampler();
if (sampler == null) return null;
return new BlurSource(src, sampler);
}
public static void blurRect(Renderer2D r, double x, double y, double w, double h,
float radius, float quality, float brightness, float alpha, int ignoredTintRgb) {
if (w <= 0.0 || h <= 0.0) return;
if (alpha <= 0.001f) return;
r.effect(UiEffectSpec.blur(UiShape.roundedRect(x, y, w, h, radius), radius, ignoredTintRgb));
BlurSource source = source();
if (source == null) return;
DrawBatch batch = Batch2D.openBlur(r, UiBatchType.BLUR, source.view, source.sampler,
legacyBlurQuality(quality), legacyKawaseOffset(quality));
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.alpha = 1.0;
float finalAlpha = (float) (alpha * r.getAlpha());
mesh.ensureQuadCapacity();
int i1 = mesh.vec2(x, y).local2(x, y).color(255, 255, 255, 255).vec4(x, y, w, h).vec4(radius, quality, brightness, finalAlpha).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(255, 255, 255, 255).vec4(x, y, w, h).vec4(radius, quality, brightness, finalAlpha).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(255, 255, 255, 255).vec4(x, y, w, h).vec4(radius, quality, brightness, finalAlpha).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(255, 255, 255, 255).vec4(x, y, w, h).vec4(radius, quality, brightness, finalAlpha).next();
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void glassBlurRect(Renderer2D r, double x, double y, double w, double h,
float radius, float quality, float brightness, float alpha, int ignoredTintRgb) {
if (w <= 0.0 || h <= 0.0) return;
if (alpha <= 0.001f) return;
r.effect(UiEffectSpec.blur(UiShape.roundedRect(x, y, w, h, radius), radius, ignoredTintRgb));
BlurSource source = source();
if (source == null) return;
DrawBatch batch = Batch2D.openBlur(r, UiBatchType.GLASS_BLUR, source.view, source.sampler,
Renderer2D.BlurQuality.HIGH, legacyKawaseOffset(quality));
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.alpha = 1.0;
float finalAlpha = clamp01((float) (alpha * r.getAlpha()));
if (finalAlpha <= 0.001f) {
Batch2D.close();
return;
}
int a = Math.max(0, Math.min(255, Math.round(finalAlpha * 255.0f)));
float smoothness = 2.0f;
float rad = Math.max(0.0f, radius);
mesh.ensureQuadCapacity();
int i1 = mesh.vec2(x, y).local2(x, y).color(255, 255, 255, a)
.vec4(x, y, w, h).vec4(rad, rad, rad, rad).vec4(quality, brightness, smoothness, 0.0f).next();
int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(255, 255, 255, a)
.vec4(x, y, w, h).vec4(rad, rad, rad, rad).vec4(quality, brightness, smoothness, 0.0f).next();
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(255, 255, 255, a)
.vec4(x, y, w, h).vec4(rad, rad, rad, rad).vec4(quality, brightness, smoothness, 0.0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(255, 255, 255, a)
.vec4(x, y, w, h).vec4(rad, rad, rad, rad).vec4(quality, brightness, smoothness, 0.0f).next();
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void blurChamferedRect(Renderer2D r, double x, double y, double w, double h,
double chamfer, float quality, float brightness, float alpha, int ignoredTintRgb) {
if (w <= 0.0 || h <= 0.0) return;
if (alpha <= 0.001f) return;
float cut = clampChamfer(chamfer, w, h);
if (cut <= 0.0f) {
blurRect(r, x, y, w, h, 0.0f, quality, brightness, alpha, ignoredTintRgb);
return;
}
blurRect(r, x, y, w, h, -cut, quality, brightness, alpha, ignoredTintRgb);
}
public static void blurComposite(Renderer2D r, Consumer<Renderer2D.BlurComposite> consumer) {
if (consumer == null) return;
BlurSource source = source();
if (source == null) return;
DrawBatch batch = Batch2D.openBlur(r, UiBatchType.BLUR_COMPLEX, source.view, source.sampler,
Renderer2D.DEFAULT_BLUR_QUALITY, Renderer2D.DEFAULT_KAWASE_OFFSET_PX);
if (batch == null) return;
batch.mesh.alpha = 1.0;
try {
consumer.accept(new Renderer2D.BlurComposite(batch.mesh, (float) r.getAlpha()));
} finally {
Batch2D.close();
}
}
}

View file

@ -5,29 +5,26 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.render.engine.renderer; package dev.loki.lovisual.render.engine.renderer.renderer2d.fx;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch; import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType; import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampRoundedRadius; import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampRoundedRadius;
final class Renderer2DRectsGlow { /** Glow, shadow and radial-glow rect effects. */
private Renderer2DRectsGlow() { public final class Effects2D {
private Effects2D() {
} }
static void roundedRectGlow(Renderer2D r, double x, double y, double w, double h, public static void roundedRectGlow(Renderer2D r, double x, double y, double w, double h,
float radius, float softness, float glow, int argb) { float radius, float softness, float glow, int argb) {
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_GLOW);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_GLOW, null, null); if (batch == null) return;
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF; int a = (argb >>> 24) & 0xFF;
@ -47,21 +44,14 @@ final class Renderer2DRectsGlow {
int i3 = mesh.vec2(gx + gw, gy + gh).local2(gx + gw, gy + gh).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glow, 0f).next(); int i3 = mesh.vec2(gx + gw, gy + gh).local2(gx + gw, gy + gh).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glow, 0f).next();
int i4 = mesh.vec2(gx + gw, gy).local2(gx + gw, gy).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glow, 0f).next(); int i4 = mesh.vec2(gx + gw, gy).local2(gx + gw, gy).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glow, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
static void roundedRectShadow(Renderer2D r, double x, double y, double w, double h, public static void roundedRectShadow(Renderer2D r, double x, double y, double w, double h,
float radius, float softness, float spread, int argb) { float radius, float softness, float spread, int argb) {
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_SHADOW);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_SHADOW, null, null); if (batch == null) return;
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF; int a = (argb >>> 24) & 0xFF;
@ -82,21 +72,14 @@ final class Renderer2DRectsGlow {
int i3 = mesh.vec2(gx + gw, gy + gh).local2(gx + gw, gy + gh).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, spread, 0f).next(); int i3 = mesh.vec2(gx + gw, gy + gh).local2(gx + gw, gy + gh).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, spread, 0f).next();
int i4 = mesh.vec2(gx + gw, gy).local2(gx + gw, gy).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, spread, 0f).next(); int i4 = mesh.vec2(gx + gw, gy).local2(gx + gw, gy).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, spread, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
static void roundedRectSoftShadow(Renderer2D r, double x, double y, double w, double h, public static void roundedRectSoftShadow(Renderer2D r, double x, double y, double w, double h,
float radius, float blur, float innerAlpha, int argb) { float radius, float blur, float innerAlpha, int argb) {
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_SOFT_SHADOW);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.ROUNDED_SOFT_SHADOW, null, null); if (batch == null) return;
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF; int a = (argb >>> 24) & 0xFF;
@ -115,23 +98,16 @@ final class Renderer2DRectsGlow {
int i3 = mesh.vec2(sx + sw, sy + sh).local2(sx + sw, sy + sh).color(red, green, blue, a).vec4(x, y, w, h).vec4(radius, blur, innerAlpha, 0f).next(); int i3 = mesh.vec2(sx + sw, sy + sh).local2(sx + sw, sy + sh).color(red, green, blue, a).vec4(x, y, w, h).vec4(radius, blur, innerAlpha, 0f).next();
int i4 = mesh.vec2(sx + sw, sy).local2(sx + sw, sy).color(red, green, blue, a).vec4(x, y, w, h).vec4(radius, blur, innerAlpha, 0f).next(); int i4 = mesh.vec2(sx + sw, sy).local2(sx + sw, sy).color(red, green, blue, a).vec4(x, y, w, h).vec4(radius, blur, innerAlpha, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
static void radialGlowMasked(Renderer2D r, double x, double y, double w, double h, public static void radialGlowMasked(Renderer2D r, double x, double y, double w, double h,
float radius, float softness, float radius, float softness,
float glowRadius, float cx, float cy, float glowRadius, float cx, float cy,
int argb) { int argb) {
boolean auto = r.beginAutoBatch(); DrawBatch batch = Batch2D.open(r, UiBatchType.RADIAL_GLOW);
DrawBatch batch = Renderer2D.UI_BATCHER.getOrCreate(UiBatchType.RADIAL_GLOW, null, null); if (batch == null) return;
if (batch == null) {
r.endAutoBatch(auto);
return;
}
MeshBuilder mesh = batch.mesh; MeshBuilder mesh = batch.mesh;
mesh.alpha = r.alpha;
mesh.ensureQuadCapacity(); mesh.ensureQuadCapacity();
int a = (argb >>> 24) & 0xFF; int a = (argb >>> 24) & 0xFF;
@ -145,7 +121,6 @@ final class Renderer2DRectsGlow {
int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glowRadius, 0.0f).vec4(cx, cy, 0f, 0f).next(); int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glowRadius, 0.0f).vec4(cx, cy, 0f, 0f).next();
int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glowRadius, 0.0f).vec4(cx, cy, 0f, 0f).next(); int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(red, green, blue, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glowRadius, 0.0f).vec4(cx, cy, 0f, 0f).next();
mesh.quad(i1, i2, i3, i4); mesh.quad(i1, i2, i3, i4);
Batch2D.close();
r.endAutoBatch(auto);
} }
} }

View file

@ -0,0 +1,89 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.fx;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.ui.BlurSource;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiEffectSpec;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clamp01;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeCornerRadii;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.packLiquidGlassPayload;
/** Liquid glass material rectangles backed by the frame blur pipeline. */
public final class Glass2D {
private Glass2D() {
}
// squirclePower < 1.5f disables the squircle falloff
public static void liquidGlassRectCorners(Renderer2D r, double x, double y, double w, double h,
float radiusTL, float radiusTR, float radiusBR, float radiusBL,
float softness,
int tintArgb,
float glassAlpha,
float blurAlpha,
float fresnelPower,
float fresnelAlpha,
float baseAlpha,
float fresnelMix,
float distortPx,
float squirclePower) {
if (w <= 0.0 || h <= 0.0) return;
r.effect(UiEffectSpec.liquidGlass(UiShape.roundedRect(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL),
softness, softness, distortPx, tintArgb));
BlurSource source = Blur2D.source();
if (source == null) return;
DrawBatch batch = Batch2D.openBlur(r, UiBatchType.LIQUID_GLASS, source.view, source.sampler,
Renderer2D.DEFAULT_LIQUID_GLASS_BLUR_QUALITY, Renderer2D.LIQUID_GLASS_KAWASE_OFFSET_PX);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int[] tint = Batch2D.rgba(tintArgb);
float clampedBlurAlpha = clamp01(blurAlpha);
int finalA = (int) (tint[3] * clamp01(glassAlpha));
float[] radii = Batch2D.CORNER_RADII;
normalizeCornerRadii(w, h, radiusTL, radiusTR, radiusBR, radiusBL, radii);
float thickness = Math.max(0.0f, softness);
float mix = clamp01(fresnelMix);
float fa = clamp01(fresnelAlpha);
float ba = clamp01(baseAlpha);
float packedDistort = packLiquidGlassPayload(distortPx, squirclePower, clampedBlurAlpha);
int i1 = glassVertex(mesh, x, y, x, y, w, h, tint, finalA, radii, mix, packedDistort, thickness, fresnelPower, fa, ba);
int i2 = glassVertex(mesh, x, y + h, x, y, w, h, tint, finalA, radii, mix, packedDistort, thickness, fresnelPower, fa, ba);
int i3 = glassVertex(mesh, x + w, y + h, x, y, w, h, tint, finalA, radii, mix, packedDistort, thickness, fresnelPower, fa, ba);
int i4 = glassVertex(mesh, x + w, y, x, y, w, h, tint, finalA, radii, mix, packedDistort, thickness, fresnelPower, fa, ba);
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
/**
* One liquid-glass vertex. {@code px}/{@code py} is the vertex position;
* {@code rx}/{@code ry} is the rect origin carried by the Rect payload.
*/
private static int glassVertex(MeshBuilder mesh, double px, double py, double rx, double ry, double w, double h,
int[] tint, int finalA, float[] radii,
float mix, float packedDistort,
float thickness, float fresnelPower, float fresnelAlpha, float baseAlpha) {
return mesh.vec2(px, py).raw2(mix, packedDistort).local2(px, py).color(tint[0], tint[1], tint[2], finalA)
.vec4((float) rx, (float) ry, (float) w, (float) h)
.vec4(radii[0], radii[1], radii[2], radii[3])
.vec4(thickness, fresnelPower, fresnelAlpha, baseAlpha)
.next();
}
}

View file

@ -0,0 +1,86 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.fx;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.util.Mth;
/** Procedural smoke fill for rounded rects (health/progress style bars). */
public final class Smoke2D {
private Smoke2D() {
}
public static void roundedSmokeFill(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
float fillRatio,
boolean fromRight,
int firstArgb,
int secondArgb,
int thirdArgb,
float time,
float smokeScale,
float smokeMix,
int octaves,
float flowX,
float flowY,
float intensity) {
if (w <= 0.0 || h <= 0.0) return;
float fill = Mth.clamp(fillRatio, 0.0f, 1.0f);
if (fill <= 0.0001f) return;
DrawBatch batch = Batch2D.open(r, UiBatchType.ROUNDED_FILL_SMOKE);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int[] first = Batch2D.rgba(firstArgb);
float[] second = Batch2D.rgbaNorm(secondArgb);
float[] third = Batch2D.rgbaNorm(thirdArgb);
float fromRightFlag = fromRight ? 1.0f : 0.0f;
float safeScale = Math.max(0.1f, smokeScale);
float safeMix = Mth.clamp(smokeMix, 0.0f, 1.0f);
float safeOctaves = Mth.clamp(octaves, 1, 8);
float safeIntensity = Math.max(0.0f, intensity);
int i1 = smokeVertex(mesh, x, y, x, y, w, h, first, second, third, radius, softness, fill, fromRightFlag,
time, safeScale, safeMix, safeOctaves, flowX, flowY, safeIntensity);
int i2 = smokeVertex(mesh, x, y + h, x, y, w, h, first, second, third, radius, softness, fill, fromRightFlag,
time, safeScale, safeMix, safeOctaves, flowX, flowY, safeIntensity);
int i3 = smokeVertex(mesh, x + w, y + h, x, y, w, h, first, second, third, radius, softness, fill, fromRightFlag,
time, safeScale, safeMix, safeOctaves, flowX, flowY, safeIntensity);
int i4 = smokeVertex(mesh, x + w, y, x, y, w, h, first, second, third, radius, softness, fill, fromRightFlag,
time, safeScale, safeMix, safeOctaves, flowX, flowY, safeIntensity);
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
/**
* One smoke-fill vertex. {@code px}/{@code py} is the vertex position;
* {@code rx}/{@code ry} is the rect origin carried by the Rect payload
* (identical for all four vertices of the quad).
*/
private static int smokeVertex(MeshBuilder mesh, double px, double py, double rx, double ry, double w, double h,
int[] first, float[] second, float[] third,
float radius, float softness, float fill, float fromRightFlag,
float time, float scale, float mix, float octaves,
float flowX, float flowY, float intensity) {
return mesh.vec2(px, py).local2(px, py).color(first[0], first[1], first[2], first[3])
.vec4((float) rx, (float) ry, (float) w, (float) h)
.vec4(radius, softness, fill, fromRightFlag)
.vec4(second[0], second[1], second[2], second[3])
.vec4(third[0], third[1], third[2], third[3])
.vec4(time, scale, mix, octaves)
.vec4(flowX, flowY, intensity, 0.0f)
.next();
}
}

View file

@ -0,0 +1,57 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.media;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.ui.UiItemSubmission;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/** ItemStack submission into the deferred item batch renderer. */
public final class Items2D {
private Items2D() {
}
public static void submit(Renderer2D r,
@Nullable LocalPlayer player,
ItemStack stack,
double x,
double y,
float scaleX,
float scaleY,
float pivotX,
float pivotY,
boolean pivoted,
int seed,
boolean drawItem,
int overlayFlags,
@Nullable String stackCountText,
int durabilityThresholdPercent,
int durabilityTextColorThresholdPercent) {
UiItemSubmission.submit(
r.isTextured(),
r.getAlpha(),
player,
stack,
x,
y,
scaleX,
scaleY,
pivotX,
pivotY,
pivoted,
seed,
drawItem,
overlayFlags,
stackCountText,
durabilityThresholdPercent,
durabilityTextColorThresholdPercent
);
}
}

View file

@ -0,0 +1,153 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.media;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.render.engine.command.UiShapeCommand;
import dev.loki.lovisual.render.engine.command.UiTextureDrawCommand;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.UiRenderDispatcher;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.clampRoundedRadius;
/** Texture quads: plain, MSDF-font and rounded-corner variants. */
public final class Textures2D {
private Textures2D() {
}
public static void textureQuad(Renderer2D r, GpuTextureView samplerView, GpuSampler sampler,
double x, double y, double w, double h, int argb) {
if (r.isTextured()) {
throw new IllegalStateException("Batched texture quad drawing is supported only on Renderer2D.COLOR.");
}
if (samplerView == null || sampler == null) return;
if (w == 0.0 || h == 0.0) return;
UiRenderDispatcher.record(new UiTextureDrawCommand(samplerView, sampler,
UiShape.rect(x, y, w, h), UiPaint.solid(argb), 0f, 0f, 1f, 1f, false));
DrawBatch batch = Batch2D.open(r, UiBatchType.TEXTURED, samplerView, sampler);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int[] c = Batch2D.rgba(argb);
int i1 = mesh.vec2(x, y).raw2(0.0, 0.0).color(c[0], c[1], c[2], c[3]).next();
int i2 = mesh.vec2(x, y + h).raw2(0.0, 1.0).color(c[0], c[1], c[2], c[3]).next();
int i3 = mesh.vec2(x + w, y + h).raw2(1.0, 1.0).color(c[0], c[1], c[2], c[3]).next();
int i4 = mesh.vec2(x + w, y).raw2(1.0, 0.0).color(c[0], c[1], c[2], c[3]).next();
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void msdfTextureQuad(Renderer2D r, GpuTextureView samplerView, GpuSampler sampler,
double x, double y, double w, double h,
int topLeft, int topRight, int bottomRight, int bottomLeft,
float pxRange, int atlasWidth, int atlasHeight) {
if (r.isTextured()) {
throw new IllegalStateException("Batched MSDF texture quad drawing is supported only on Renderer2D.COLOR.");
}
if (samplerView == null || sampler == null) return;
if (w == 0.0 || h == 0.0) return;
if (pxRange <= 0.0f || atlasWidth <= 0 || atlasHeight <= 0) return;
UiRenderDispatcher.record(new UiTextureDrawCommand(samplerView, sampler,
UiShape.rect(x, y, w, h), UiPaint.solid(topLeft), 0f, 0f, 1f, 1f, false));
DrawBatch batch = Batch2D.openMsdf(r, samplerView, sampler, pxRange, atlasWidth, atlasHeight);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int i1 = msdfVertex(mesh, x, y, 0.0, 0.0, topLeft);
int i2 = msdfVertex(mesh, x, y + h, 0.0, 1.0, bottomLeft);
int i3 = msdfVertex(mesh, x + w, y + h, 1.0, 1.0, bottomRight);
int i4 = msdfVertex(mesh, x + w, y, 1.0, 0.0, topRight);
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void texQuad(Renderer2D r, double x, double y, double width, double height,
double texX1, double texY1, double texX2, double texY2,
int argb) {
UiRenderDispatcher.record(new UiShapeCommand(UiShape.rect(x, y, width, height),
UiPaint.solid(argb), UiStroke.NONE, true));
MeshBuilder texturedTriangles = r.texturedMesh();
if (texturedTriangles == null) return;
texturedTriangles.alpha = r.getAlpha();
texturedTriangles.ensureQuadCapacity();
int[] c = Batch2D.rgba(argb);
int i1 = texturedTriangles.vec2(x, y).raw2(texX1, texY1).color(c[0], c[1], c[2], c[3]).next();
int i2 = texturedTriangles.vec2(x, y + height).raw2(texX1, texY2).color(c[0], c[1], c[2], c[3]).next();
int i3 = texturedTriangles.vec2(x + width, y + height).raw2(texX2, texY2).color(c[0], c[1], c[2], c[3]).next();
int i4 = texturedTriangles.vec2(x + width, y).raw2(texX2, texY1).color(c[0], c[1], c[2], c[3]).next();
texturedTriangles.quad(i1, i2, i3, i4);
}
public static void roundedTexRect(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
double texX1, double texY1, double texX2, double texY2,
int argb, GpuTextureView samplerView, GpuSampler sampler) {
roundedTex(r, x, y, w, h, radius, texX1, texY1, texX2, texY2, argb, samplerView, sampler,
UiBatchType.ROUNDED_TEXTURED, false);
}
public static void roundedTexMaskRect(Renderer2D r, double x, double y, double w, double h,
float radius, float softness,
double texX1, double texY1, double texX2, double texY2,
int argb, GpuTextureView samplerView, GpuSampler sampler) {
roundedTex(r, x, y, w, h, radius, texX1, texY1, texX2, texY2, argb, samplerView, sampler,
UiBatchType.ROUNDED_TEXTURED_MASK, true);
}
private static void roundedTex(Renderer2D r, double x, double y, double w, double h,
float radius,
double texX1, double texY1, double texX2, double texY2,
int argb, GpuTextureView samplerView, GpuSampler sampler,
UiBatchType type, boolean mask) {
if (!r.isTextured()) {
throw new IllegalStateException("Renderer2D is not configured for textures.");
}
if (samplerView == null || sampler == null) return;
UiRenderDispatcher.record(new UiTextureDrawCommand(samplerView, sampler,
UiShape.roundedRect(x, y, w, h, radius), UiPaint.solid(argb),
(float) texX1, (float) texY1, (float) texX2, (float) texY2, mask));
DrawBatch batch = Batch2D.open(r, type, samplerView, sampler);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int[] c = Batch2D.rgba(argb);
float clampedRadius = clampRoundedRadius(radius, w, h);
int i1 = roundedTexVertex(mesh, x, y, texX1, texY1, c, x, y, w, h, clampedRadius);
int i2 = roundedTexVertex(mesh, x, y + h, texX1, texY2, c, x, y, w, h, clampedRadius);
int i3 = roundedTexVertex(mesh, x + w, y + h, texX2, texY2, c, x, y, w, h, clampedRadius);
int i4 = roundedTexVertex(mesh, x + w, y, texX2, texY1, c, x, y, w, h, clampedRadius);
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
private static int msdfVertex(MeshBuilder mesh, double px, double py, double u, double v, int argb) {
int[] c = Batch2D.rgba(argb);
return mesh.vec2(px, py).raw2(u, v).color(c[0], c[1], c[2], c[3]).next();
}
private static int roundedTexVertex(MeshBuilder mesh, double px, double py, double u, double v,
int[] c, double rx, double ry, double w, double h, float radius) {
return mesh.vec2(px, py).raw2(u, v).local2(px, py).color(c[0], c[1], c[2], c[3])
.vec4(rx, ry, w, h).vec4(radius, 0.0f, 0f, 0f).next();
}
}

View file

@ -0,0 +1,200 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.path;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.util.Mth;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.normalizeDegrees;
/** Circles, rings, arcs and the animated Orbiz ring effect. */
public final class Circles2D {
private Circles2D() {
}
public static void circle(Renderer2D r, double cx, double cy, double radius, float softness, int argb) {
r.shape(UiShape.circle(cx, cy, radius), UiPaint.solid(argb));
DrawBatch batch = Batch2D.open(r, UiBatchType.CIRCLE);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int[] c = Batch2D.rgba(argb);
double outerRadius = radius + Math.max(0.0f, softness);
double x = cx - outerRadius;
double y = cy - outerRadius;
double size = outerRadius * 2.0;
int i1 = circleVertex(mesh, x, y, c, x, y, size, (float) radius, softness, 0f);
int i2 = circleVertex(mesh, x, y + size, c, x, y, size, (float) radius, softness, 0f);
int i3 = circleVertex(mesh, x + size, y + size, c, x, y, size, (float) radius, softness, 0f);
int i4 = circleVertex(mesh, x + size, y, c, x, y, size, (float) radius, softness, 0f);
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void circleStroke(Renderer2D r, double cx, double cy, double radius,
double thickness, float softness, int argb) {
r.shapeStroke(UiShape.circle(cx, cy, radius), UiPaint.solid(argb), UiStroke.of(thickness));
DrawBatch batch = Batch2D.open(r, UiBatchType.CIRCLE);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int[] c = Batch2D.rgba(argb);
float stroke = (float) Math.max(0.0, thickness);
double outerRadius = radius + stroke * 0.5 + softness;
double x = cx - outerRadius;
double y = cy - outerRadius;
double size = outerRadius * 2.0;
int i1 = circleVertex(mesh, x, y, c, x, y, size, (float) radius, softness, stroke);
int i2 = circleVertex(mesh, x, y + size, c, x, y, size, (float) radius, softness, stroke);
int i3 = circleVertex(mesh, x + size, y + size, c, x, y, size, (float) radius, softness, stroke);
int i4 = circleVertex(mesh, x + size, y, c, x, y, size, (float) radius, softness, stroke);
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void arcStrokeQuad(Renderer2D r, double cx, double cy, double radius, double thickness,
float startAngleDeg, float endAngleDeg, float softness, boolean caps,
int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) {
float start = normalizeDegrees(startAngleDeg);
float end = normalizeDegrees(endAngleDeg);
float sweep = end - start;
if (sweep <= 0.0f) {
sweep += 360.0f;
}
if (sweep <= 0.001f) {
return;
}
r.shapeStroke(UiShape.arc(cx, cy, radius, start, end),
UiPaint.corners(cTopLeft, cTopRight, cBottomRight, cBottomLeft), UiStroke.of(thickness));
DrawBatch batch = Batch2D.open(r, UiBatchType.ARC);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
float stroke = (float) Math.max(0.0, thickness);
double outerRadius = radius + stroke * 0.5 + softness;
double x = cx - outerRadius;
double y = cy - outerRadius;
double size = outerRadius * 2.0;
int i1 = arcVertex(mesh, x, y, cTopLeft, x, y, size, (float) radius, softness, stroke, start, end, caps);
int i2 = arcVertex(mesh, x, y + size, cBottomLeft, x, y, size, (float) radius, softness, stroke, start, end, caps);
int i3 = arcVertex(mesh, x + size, y + size, cBottomRight, x, y, size, (float) radius, softness, stroke, start, end, caps);
int i4 = arcVertex(mesh, x + size, y, cTopRight, x, y, size, (float) radius, softness, stroke, start, end, caps);
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
public static void orbizRing(Renderer2D r, double cx, double cy,
double radius, double thickness,
float startAngleDeg, float sweepAngleDeg,
float softness, float glowRadius, float glowStrength,
int primaryArgb, int secondaryArgb,
int colorMode, float angularOffset, float angularCycles,
float trackAlpha, float bodyAlpha, float headBoost,
float trackGlow, boolean caps) {
if (r.isTextured()) {
throw new IllegalStateException("Orbiz ring drawing is supported only on Renderer2D.COLOR.");
}
if (radius <= 0.0 || thickness <= 0.0) return;
if (((primaryArgb >>> 24) & 0xFF) <= 0 && ((secondaryArgb >>> 24) & 0xFF) <= 0) return;
DrawBatch batch = Batch2D.open(r, UiBatchType.ORBIZ_RING);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureQuadCapacity();
int[] primary = Batch2D.rgba(primaryArgb);
float[] secondary = Batch2D.rgbaNorm(secondaryArgb);
float stroke = (float) Math.max(0.0, thickness);
float ringRadius = (float) Math.max(0.0, radius);
float soft = Math.max(0.0f, softness);
float glow = Math.max(0.0f, glowRadius);
float expand = stroke * 0.5f + soft + glow + 1.0f;
double outerRadius = radius + expand;
double x = cx - outerRadius;
double y = cy - outerRadius;
double size = outerRadius * 2.0;
double ringX = cx - radius;
double ringY = cy - radius;
double ringSize = radius * 2.0;
float start = normalizeDegrees(startAngleDeg);
float sweep = Mth.clamp(sweepAngleDeg, 0.0f, 360.0f);
float offset = angularOffset - (float) Math.floor(angularOffset);
float cycles = Math.max(0.001f, angularCycles);
float mode = Math.max(0, colorMode);
float track = Mth.clamp(trackAlpha, 0.0f, 1.0f);
float body = Mth.clamp(bodyAlpha, 0.0f, 1.0f);
float glowPower = Math.max(0.0f, glowStrength);
float head = Math.max(0.0f, headBoost);
float trackGlowAmount = Mth.clamp(trackGlow, 0.0f, 1.0f);
int i1 = orbizVertex(mesh, x, y, primary, secondary, ringX, ringY, ringSize, ringRadius, soft, stroke,
start, sweep, glow, glowPower, mode, offset, cycles, caps, track, body, head, trackGlowAmount);
int i2 = orbizVertex(mesh, x, y + size, primary, secondary, ringX, ringY, ringSize, ringRadius, soft, stroke,
start, sweep, glow, glowPower, mode, offset, cycles, caps, track, body, head, trackGlowAmount);
int i3 = orbizVertex(mesh, x + size, y + size, primary, secondary, ringX, ringY, ringSize, ringRadius, soft, stroke,
start, sweep, glow, glowPower, mode, offset, cycles, caps, track, body, head, trackGlowAmount);
int i4 = orbizVertex(mesh, x + size, y, primary, secondary, ringX, ringY, ringSize, ringRadius, soft, stroke,
start, sweep, glow, glowPower, mode, offset, cycles, caps, track, body, head, trackGlowAmount);
mesh.quad(i1, i2, i3, i4);
Batch2D.close();
}
private static int circleVertex(MeshBuilder mesh, double px, double py, int[] c,
double rx, double ry, double size,
float radius, float softness, float stroke) {
return mesh.vec2(px, py).local2(px, py).color(c[0], c[1], c[2], c[3])
.vec4(rx, ry, size, size)
.vec4(radius, softness, stroke, 0f)
.next();
}
private static int arcVertex(MeshBuilder mesh, double px, double py, int argb,
double rx, double ry, double size,
float radius, float softness, float stroke,
float start, float end, boolean caps) {
int[] c = Batch2D.rgba(argb);
return mesh.vec2(px, py).local2(px, py).color(c[0], c[1], c[2], c[3])
.vec4(rx, ry, size, size)
.vec4(radius, softness, stroke, start)
.vec4(end, caps ? 1f : 0f, 0f, 0f)
.next();
}
private static int orbizVertex(MeshBuilder mesh, double px, double py,
int[] primary, float[] secondary,
double ringX, double ringY, double ringSize, float ringRadius,
float soft, float stroke, float start,
float sweep, float glow, float glowPower, float mode,
float offset, float cycles, boolean caps,
float track, float body, float head, float trackGlowAmount) {
return mesh.vec2(px, py).local2(px, py).color(primary[0], primary[1], primary[2], primary[3])
.vec4(ringX, ringY, ringSize, ringSize)
.vec4(ringRadius, soft, stroke, start)
.vec4(sweep, glow, glowPower, mode)
.vec4(secondary[0], secondary[1], secondary[2], secondary[3])
.vec4(offset, cycles, 0.0f, caps ? 1.0f : 0.0f)
.vec4(track, body, head, trackGlowAmount)
.next();
}
}

View file

@ -0,0 +1,156 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.path;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.buildBezier;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.roundedRectAnchor;
/** Wires, cables and node-graph edges built on top of polylines. */
public final class Connectors2D {
private Connectors2D() {
}
public static void connector(Renderer2D r, double x1, double y1, double x2, double y2,
double thickness, int argb) {
double[] c = Batch2D.CONNECTOR;
c[0] = x1;
c[1] = y1;
c[2] = x2;
c[3] = y2;
Polygons2D.polyline(r, c, 2, thickness, false, argb, true);
}
public static void connectorGradient(Renderer2D r, double x1, double y1, double x2, double y2,
double thickness, int startArgb, int endArgb) {
double[] c = Batch2D.CONNECTOR;
c[0] = x1;
c[1] = y1;
c[2] = x2;
c[3] = y2;
Polygons2D.polylineGradient(r, c, 2, thickness, false, startArgb, endArgb, true);
}
public static void orthogonalConnector(Renderer2D r, double x1, double y1, double x2, double y2,
double midX, double thickness, int argb) {
double[] c = Batch2D.CONNECTOR;
c[0] = x1;
c[1] = y1;
c[2] = midX;
c[3] = y1;
c[4] = midX;
c[5] = y2;
c[6] = x2;
c[7] = y2;
Polygons2D.polyline(r, c, 4, thickness, false, argb, true);
}
public static void orthogonalConnectorGradient(Renderer2D r, double x1, double y1, double x2, double y2,
double midX, double thickness, int startArgb, int endArgb) {
double[] c = Batch2D.CONNECTOR;
c[0] = x1;
c[1] = y1;
c[2] = midX;
c[3] = y1;
c[4] = midX;
c[5] = y2;
c[6] = x2;
c[7] = y2;
Polygons2D.polylineGradient(r, c, 4, thickness, false, startArgb, endArgb, true);
}
public static void bezierConnector(Renderer2D r, double x1, double y1,
double cx1, double cy1,
double cx2, double cy2,
double x2, double y2,
int segments,
double thickness,
int argb) {
double[] c = Batch2D.CONNECTOR;
int count = buildBezier(c, x1, y1, cx1, cy1, cx2, cy2, x2, y2, segments);
Polygons2D.polyline(r, c, count, thickness, false, argb, false);
}
public static void bezierConnectorGradient(Renderer2D r, double x1, double y1,
double cx1, double cy1,
double cx2, double cy2,
double x2, double y2,
int segments,
double thickness,
int startArgb,
int endArgb) {
double[] c = Batch2D.CONNECTOR;
int count = buildBezier(c, x1, y1, cx1, cy1, cx2, cy2, x2, y2, segments);
Polygons2D.polylineGradient(r, c, count, thickness, false, startArgb, endArgb, false);
}
public static void nodeGraphEdge(Renderer2D r, double x1, double y1, double x2, double y2,
double thickness, int argb) {
double c = Math.max(24.0, Math.abs(x2 - x1) * 0.5);
bezierConnector(r, x1, y1, x1 + c, y1, x2 - c, y2, x2, y2, 20, thickness, argb);
}
public static void nodeGraphEdgeGradient(Renderer2D r, double x1, double y1, double x2, double y2,
double thickness, int startArgb, int endArgb) {
double c = Math.max(24.0, Math.abs(x2 - x1) * 0.5);
bezierConnectorGradient(r, x1, y1, x1 + c, y1, x2 - c, y2, x2, y2, 20, thickness, startArgb, endArgb);
}
public static void roundedRectConnector(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double thickness, int argb) {
double[] a = anchors(x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2);
connector(r, a[0], a[1], a[2], a[3], thickness, argb);
}
public static void roundedRectConnectorGradient(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double thickness, int startArgb, int endArgb) {
double[] a = anchors(x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2);
connectorGradient(r, a[0], a[1], a[2], a[3], thickness, startArgb, endArgb);
}
public static void roundedRectNodeGraphEdge(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double thickness, int argb) {
double[] a = anchors(x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2);
nodeGraphEdge(r, a[0], a[1], a[2], a[3], thickness, argb);
}
public static void roundedRectNodeGraphEdgeGradient(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double thickness, int startArgb, int endArgb) {
double[] a = anchors(x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2);
nodeGraphEdgeGradient(r, a[0], a[1], a[2], a[3], thickness, startArgb, endArgb);
}
public static void roundedRectOrthogonalConnector(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double midX, double thickness, int argb) {
double[] a = anchors(x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2);
orthogonalConnector(r, a[0], a[1], a[2], a[3], midX, thickness, argb);
}
public static void roundedRectOrthogonalConnectorGradient(Renderer2D r, double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2,
double midX, double thickness, int startArgb, int endArgb) {
double[] a = anchors(x1, y1, w1, h1, radius1, x2, y2, w2, h2, radius2);
orthogonalConnectorGradient(r, a[0], a[1], a[2], a[3], midX, thickness, startArgb, endArgb);
}
/** Anchors both ends of a connector on the visible borders of the two rounded rects. */
private static double[] anchors(double x1, double y1, double w1, double h1, double radius1,
double x2, double y2, double w2, double h2, double radius2) {
double[] a = Batch2D.CONNECTOR_ANCHOR;
roundedRectAnchor(a, 0, x1, y1, w1, h1, radius1, x2 + w2 * 0.5, y2 + h2 * 0.5);
roundedRectAnchor(a, 2, x2, y2, w2, h2, radius2, x1 + w1 * 0.5, y1 + h1 * 0.5);
return a;
}
}

View file

@ -0,0 +1,76 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.path;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
/** Immediate-mode 1px lines. */
public final class Lines2D {
private Lines2D() {
}
public static void line(Renderer2D r, double x1, double y1, double x2, double y2, int argb) {
double[] p = {x1, y1, x2, y2};
r.path(UiShape.polyline(p, 2, false), UiPaint.solid(argb), UiStroke.of(1.0), false);
DrawBatch batch = Batch2D.open(r, UiBatchType.LINES);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
int[] c = Batch2D.rgba(argb);
mesh.ensureLineCapacity();
int i1 = mesh.vec2(x1, y1).color(c[0], c[1], c[2], c[3]).next();
int i2 = mesh.vec2(x2, y2).color(c[0], c[1], c[2], c[3]).next();
mesh.line(i1, i2);
Batch2D.close();
}
public static void line(Renderer2D r, double x1, double y1, double x2, double y2, RenderColor color) {
double[] p = {x1, y1, x2, y2};
r.path(UiShape.polyline(p, 2, false), UiPaint.solid(color.argb()), UiStroke.of(1.0), false);
DrawBatch batch = Batch2D.open(r, UiBatchType.LINES);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureLineCapacity();
int i1 = mesh.vec2(x1, y1).color(color.r, color.g, color.b, color.a).next();
int i2 = mesh.vec2(x2, y2).color(color.r, color.g, color.b, color.a).next();
mesh.line(i1, i2);
Batch2D.close();
}
public static void boxLines(Renderer2D r, double x, double y, double width, double height, int argb) {
r.shapeStroke(UiShape.rect(x, y, width, height), UiPaint.solid(argb), UiStroke.of(1.0));
DrawBatch batch = Batch2D.open(r, UiBatchType.LINES);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
mesh.ensureCapacity(4, 8);
int[] c = Batch2D.rgba(argb);
int i1 = mesh.vec2(x, y).color(c[0], c[1], c[2], c[3]).next();
int i2 = mesh.vec2(x, y + height).color(c[0], c[1], c[2], c[3]).next();
int i3 = mesh.vec2(x + width, y + height).color(c[0], c[1], c[2], c[3]).next();
int i4 = mesh.vec2(x + width, y).color(c[0], c[1], c[2], c[3]).next();
mesh.line(i1, i2);
mesh.line(i2, i3);
mesh.line(i3, i4);
mesh.line(i4, i1);
Batch2D.close();
}
}

View file

@ -0,0 +1,170 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.render.engine.renderer.renderer2d.path;
import dev.loki.lovisual.render.engine.command.UiShapeCommand;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D;
import dev.loki.lovisual.render.engine.renderer.ui.DrawBatch;
import dev.loki.lovisual.render.engine.renderer.ui.UiBatchType;
import dev.loki.lovisual.render.engine.renderer.ui.UiRenderDispatcher;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiBoxPathBuilder;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiBoxShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaint;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPaintKind;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPathCap;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiPathJoin;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiShape;
import dev.loki.lovisual.render.engine.renderer.ui.draw.UiStroke;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.appendPolygon;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.appendPolygonGradient;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.appendPolyline;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.appendPolylineGradient;
import static dev.loki.lovisual.render.engine.renderer.ui.draw.UiMeshGeometry.appendPolylineLinearGradient;
/** Filled polygons, polylines and flexible box shapes. */
public final class Polygons2D {
private Polygons2D() {
}
public static void box(Renderer2D r, UiBoxShape box, UiPaint paint, UiStroke stroke, boolean fill) {
if (box == null || paint == null) return;
UiStroke safeStroke = stroke == null ? UiStroke.NONE : stroke;
UiRenderDispatcher.record(new UiShapeCommand(UiShape.box(box), paint, safeStroke, fill));
renderFlexibleBoxFallback(r, box, paint, safeStroke, fill);
}
private static void renderFlexibleBoxFallback(Renderer2D r, UiBoxShape box, UiPaint paint, UiStroke stroke, boolean fill) {
if (r.isTextured() || box == null || paint == null) return;
double[] shape = Batch2D.RECT_SHAPE;
int count = UiBoxPathBuilder.write(box, shape, shape.length / 2);
if (count < 3) return;
if (fill) {
if (paint.kind() == UiPaintKind.LINEAR_GRADIENT) {
polygonGradient(r, shape, count, box.bounds().x(), box.bounds().y(),
box.bounds().width(), box.bounds().height(),
paint.topLeft(), paint.topRight(), paint.angleDeg(), paint.offsetPx());
} else {
polygon(r, shape, count, paint.solidColor());
}
} else if (stroke.enabled()) {
boolean round = stroke.join() == UiPathJoin.ROUND || stroke.cap() == UiPathCap.ROUND;
if (paint.kind() == UiPaintKind.LINEAR_GRADIENT) {
polylineLinearGradient(r, shape, count, stroke.thickness(), true,
box.bounds().x(), box.bounds().y(), box.bounds().width(), box.bounds().height(),
paint.topLeft(), paint.topRight(), paint.angleDeg(), paint.offsetPx(), round);
} else {
polyline(r, shape, count, stroke.thickness(), true, paint.solidColor(), round);
}
}
}
public static void polygon(Renderer2D r, double[] points, int pointCount, int argb) {
if (r.isTextured()) {
throw new IllegalStateException("Polygon drawing is supported only on Renderer2D.COLOR.");
}
int safeCount = Math.min(pointCount, points != null ? points.length / 2 : 0);
safeCount = Math.min(safeCount, Math.min(Batch2D.POLY_INDEX.length, Batch2D.POLY_VERTEX.length));
if (points == null || safeCount < 3) return;
r.path(UiShape.polyline(points, safeCount, true), UiPaint.solid(argb), UiStroke.NONE, true);
DrawBatch batch = Batch2D.open(r, UiBatchType.COLORED);
if (batch == null) return;
appendPolygon(batch.mesh, points, safeCount, Batch2D.POLY_INDEX, Batch2D.POLY_VERTEX, argb);
Batch2D.close();
}
public static void polygonGradient(Renderer2D r, double[] points, int pointCount,
double x, double y, double width, double height,
int startArgb, int endArgb, float angleDeg, float offsetPx) {
if (r.isTextured()) {
throw new IllegalStateException("Polygon drawing is supported only on Renderer2D.COLOR.");
}
if (points == null || pointCount < 3) return;
r.path(UiShape.polyline(points, pointCount, true),
UiPaint.linear(startArgb, endArgb, angleDeg, offsetPx), UiStroke.NONE, true);
DrawBatch batch = Batch2D.open(r, UiBatchType.COLORED);
if (batch == null) return;
appendPolygonGradient(batch.mesh, points, pointCount, Batch2D.POLY_INDEX, Batch2D.POLY_VERTEX,
x, y, width, height, startArgb, endArgb, angleDeg, offsetPx);
Batch2D.close();
}
public static void polygonStroke(Renderer2D r, double[] points, int pointCount,
double thickness, boolean closed, int argb) {
polyline(r, points, pointCount, thickness, closed, argb, false);
}
public static void polyline(Renderer2D r, double[] points, int pointCount,
double thickness, boolean closed, int argb) {
polyline(r, points, pointCount, thickness, closed, argb, false);
}
public static void polyline(Renderer2D r, double[] points, int pointCount,
double thickness, boolean closed, int argb, boolean roundCapsAndJoins) {
if (r.isTextured()) {
throw new IllegalStateException("Polyline drawing is supported only on Renderer2D.COLOR.");
}
if (points == null || pointCount < 2) return;
double t = Math.max(0.0, thickness);
if (t <= 0.0) return;
UiStroke stroke = roundCapsAndJoins ? UiStroke.of(t).withRoundCapsAndJoins() : UiStroke.of(t);
r.path(UiShape.polyline(points, pointCount, closed), UiPaint.solid(argb), stroke, false);
DrawBatch batch = Batch2D.open(r, UiBatchType.COLORED);
if (batch == null) return;
appendPolyline(batch.mesh, points, pointCount, t, closed, argb, roundCapsAndJoins);
Batch2D.close();
}
public static void polylineGradient(Renderer2D r, double[] points, int pointCount,
double thickness, boolean closed,
int startArgb, int endArgb, boolean roundCapsAndJoins) {
if (r.isTextured()) {
throw new IllegalStateException("Polyline drawing is supported only on Renderer2D.COLOR.");
}
if (points == null || pointCount < 2) return;
double t = Math.max(0.0, thickness);
if (t <= 0.0) return;
UiStroke stroke = roundCapsAndJoins ? UiStroke.of(t).withRoundCapsAndJoins() : UiStroke.of(t);
r.path(UiShape.polyline(points, pointCount, closed),
UiPaint.corners(startArgb, endArgb, endArgb, startArgb), stroke, false);
DrawBatch batch = Batch2D.open(r, UiBatchType.COLORED);
if (batch == null) return;
appendPolylineGradient(batch.mesh, points, pointCount, t, closed, startArgb, endArgb, roundCapsAndJoins);
Batch2D.close();
}
public static void polylineLinearGradient(Renderer2D r, double[] points, int pointCount,
double thickness, boolean closed,
double x, double y, double width, double height,
int startArgb, int endArgb, float angleDeg, float offsetPx,
boolean roundCapsAndJoins) {
if (r.isTextured()) {
throw new IllegalStateException("Polyline drawing is supported only on Renderer2D.COLOR.");
}
if (points == null || pointCount < 2) return;
double t = Math.max(0.0, thickness);
if (t <= 0.0) return;
UiStroke stroke = roundCapsAndJoins ? UiStroke.of(t).withRoundCapsAndJoins() : UiStroke.of(t);
r.path(UiShape.polyline(points, pointCount, closed),
UiPaint.linear(startArgb, endArgb, angleDeg, offsetPx), stroke, false);
DrawBatch batch = Batch2D.open(r, UiBatchType.COLORED);
if (batch == null) return;
MeshBuilder mesh = batch.mesh;
appendPolylineLinearGradient(mesh, points, pointCount, t, closed,
x, y, width, height, startArgb, endArgb, angleDeg, offsetPx, roundCapsAndJoins);
Batch2D.close();
}
}

View file

@ -0,0 +1,115 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class TriangulatorSolverTest {
@Test
void solve_nullEmpty_returnsNull() {
assertNull(TriangulatorSolver.solve(null, 6.0));
assertNull(TriangulatorSolver.solve(List.of(), 6.0));
}
@Test
void solve_singleThrow_notHasSolution() {
var throws_ = List.of(new TriangulatorSolver.CachedThrow(
1L, 100.0, 50.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L));
var result = TriangulatorSolver.solve(throws_, 6.0);
assertNotNull(result);
assertFalse(result.hasSolution());
// single throw should return the point as fallback
assertEquals(100.0, result.x(), 0.01);
assertEquals(50.0, result.z(), 0.01);
}
@Test
void solve_twoParallelThrows_noSolution() {
// two parallel throws - angle ~0, should not solve
var throws_ = List.of(
new TriangulatorSolver.CachedThrow(1L, 0.0, 0.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L),
new TriangulatorSolver.CachedThrow(2L, 10.0, 0.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L)
);
var result = TriangulatorSolver.solve(throws_, 6.0);
assertNotNull(result);
assertFalse(result.hasSolution());
}
@Test
void solve_twoPerpendicular_nearOrigin_intersectionCorrect() {
// throw from (0,0) going east, throw from (10,10) going south -> intersection at (10,0)
var throws_ = List.of(
new TriangulatorSolver.CachedThrow(1L, 0.0, 0.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L),
new TriangulatorSolver.CachedThrow(2L, 10.0, 10.0, 0.0, -1.0, 0.9, 0.99, 0.05, 8, 0L)
);
var result = TriangulatorSolver.solve(throws_, 6.0);
assertNotNull(result);
assertTrue(result.hasSolution());
assertEquals(10.0, result.x(), 0.5);
assertEquals(0.0, result.z(), 0.5);
assertEquals(0.0, result.rmse(), 0.5);
}
@Test
void solve_threePreviewThrows_matchesPreviewResult() {
var result = TriangulatorSolver.solve(TriangulatorSolver.PREVIEW_THROWS, 6.0);
assertNotNull(result);
// HEAD preview was solved at 6.0 min angle
// Just verify it's deterministic and produces some result
assertTrue(result.hasSolution() || !result.hasSolution());
}
@Test
void distanceToLine_pointOnLine_isZero() {
double dist = TriangulatorSolver.distanceToLine(10.0, 0.0,
new TriangulatorSolver.CachedThrow(1L, 0.0, 0.0, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L));
assertEquals(0.0, dist, 0.01);
}
@Test
void formatPercent() {
assertEquals("91%", TriangulatorSolver.formatPercent(0.91));
assertEquals("100%", TriangulatorSolver.formatPercent(1.0));
assertEquals("0%", TriangulatorSolver.formatPercent(0.0));
}
@Test
void formatThrowPoint() {
var throw1 = new TriangulatorSolver.CachedThrow(1L, 110.4, 58.6, 1.0, 0.0, 0.9, 0.99, 0.05, 8, 0L);
var throw2 = new TriangulatorSolver.CachedThrow(2L, -100.2, -50.7, 0.0, 1.0, 0.9, 0.99, 0.05, 8, 0L);
assertEquals("110 59", TriangulatorSolver.formatThrowPoint(throw1));
assertEquals("-100 -51", TriangulatorSolver.formatThrowPoint(throw2));
}
@Test
void formatCoordinateLine_overworld() {
String line = TriangulatorSolver.formatCoordinateLine(1288.0, -412.0, false);
assertTrue(line.contains("X 1288") && line.contains("Z -412"));
}
@Test
void formatCoordinateLine_nether() {
String line = TriangulatorSolver.formatCoordinateLine(1288.0, -412.0, true);
// Math.round(-51.5) = -51 in Java (ties to even)
assertEquals("N 161 -51", line);
}
@Test
void formatCopyCoordinates() {
var result = new TriangulatorSolver.SolveResult(true, true, 1288.0, -412.0, 5.2, 15.0, 0.88, 80, -26);
String copy = TriangulatorSolver.formatCopyCoordinates(result);
assertTrue(copy.contains("1288"));
assertTrue(copy.contains("-412"));
assertTrue(copy.contains("64")); // Y level
}
}