From 4b9e966ca7e8a9291f307850f715820e122d69fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linnea=20Gr=C3=A4f?= Date: Tue, 17 Jun 2025 20:59:45 +0200 Subject: [PATCH] feat: Add macro wheels --- docs/firmament_logo_256_trans.webp | Bin 0 -> 9972 bytes docs/firmament_logo_trans.webp | Bin 0 -> 9706 bytes .../mixins/DispatchMouseInputEventsPatch.java | 17 ++ src/main/kotlin/events/WorldKeyboardEvent.kt | 17 +- src/main/kotlin/events/WorldMouseMoveEvent.kt | 5 + .../kotlin/features/macros/KeyComboTrie.kt | 5 + src/main/kotlin/features/macros/MacroData.kt | 5 +- src/main/kotlin/features/macros/MacroUI.kt | 170 +++++++++++++++--- src/main/kotlin/features/macros/RadialMenu.kt | 149 +++++++++++++++ src/main/kotlin/keybindings/IKeyBinding.kt | 31 +++- .../kotlin/keybindings/SavedKeyBinding.kt | 7 + src/main/kotlin/util/collections/RangeUtil.kt | 40 +++++ src/main/kotlin/util/math/Projections.kt | 46 +++++ .../kotlin/util/render/CustomRenderLayers.kt | 37 +++- src/main/kotlin/util/render/DrawContextExt.kt | 11 +- src/main/kotlin/util/render/LerpUtils.kt | 35 ++-- .../util/render/RenderCircleProgress.kt | 144 ++++++++------- .../firmament/gui/config/macros/combos.xml | 1 - .../macros/{editor.xml => editor_combo.xml} | 0 .../gui/config/macros/editor_wheel.xml | 43 +++++ .../firmament/gui/config/macros/index.xml | 33 ++-- .../firmament/gui/config/macros/wheel.xml | 54 ++++++ .../shaders/circle_discard_color.fsh | 22 +++ .../kotlin/util/math/ProjectionsBoxTest.kt | 28 +++ 24 files changed, 753 insertions(+), 147 deletions(-) create mode 100644 docs/firmament_logo_256_trans.webp create mode 100644 docs/firmament_logo_trans.webp create mode 100644 src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java create mode 100644 src/main/kotlin/events/WorldMouseMoveEvent.kt create mode 100644 src/main/kotlin/features/macros/RadialMenu.kt create mode 100644 src/main/kotlin/util/collections/RangeUtil.kt create mode 100644 src/main/kotlin/util/math/Projections.kt rename src/main/resources/assets/firmament/gui/config/macros/{editor.xml => editor_combo.xml} (100%) create mode 100644 src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml create mode 100644 src/main/resources/assets/firmament/gui/config/macros/wheel.xml create mode 100644 src/main/resources/assets/firmament/shaders/circle_discard_color.fsh create mode 100644 src/test/kotlin/util/math/ProjectionsBoxTest.kt diff --git a/docs/firmament_logo_256_trans.webp b/docs/firmament_logo_256_trans.webp new file mode 100644 index 0000000000000000000000000000000000000000..6e05b838bae324d9b6b0711caa913894b0c1dfcb GIT binary patch literal 9972 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 literal 0 HcmV?d00001 diff --git a/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java b/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java new file mode 100644 index 0000000..f1b07bb --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java @@ -0,0 +1,17 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import moe.nea.firmament.events.WorldMouseMoveEvent; +import net.minecraft.client.Mouse; +import net.minecraft.client.network.ClientPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Mouse.class) +public class DispatchMouseInputEventsPatch { + @WrapWithCondition(method = "updateMouse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;changeLookDirection(DD)V")) + public boolean onRotatePlayer(ClientPlayerEntity instance, double deltaX, double deltaY) { + var event = WorldMouseMoveEvent.Companion.publish(new WorldMouseMoveEvent(deltaX, deltaY)); + return !event.getCancelled(); + } +} diff --git a/src/main/kotlin/events/WorldKeyboardEvent.kt b/src/main/kotlin/events/WorldKeyboardEvent.kt index e8566fd..1d6a758 100644 --- a/src/main/kotlin/events/WorldKeyboardEvent.kt +++ b/src/main/kotlin/events/WorldKeyboardEvent.kt @@ -1,18 +1,17 @@ - - package moe.nea.firmament.events import net.minecraft.client.option.KeyBinding import moe.nea.firmament.keybindings.IKeyBinding data class WorldKeyboardEvent(val keyCode: Int, val scanCode: Int, val modifiers: Int) : FirmamentEvent.Cancellable() { - companion object : FirmamentEventBus() + companion object : FirmamentEventBus() - fun matches(keyBinding: KeyBinding): Boolean { - return matches(IKeyBinding.minecraft(keyBinding)) - } + fun matches(keyBinding: KeyBinding): Boolean { + return matches(IKeyBinding.minecraft(keyBinding)) + } - fun matches(keyBinding: IKeyBinding): Boolean { - return keyBinding.matches(keyCode, scanCode, modifiers) - } + fun matches(keyBinding: IKeyBinding, atLeast: Boolean = false): Boolean { + return if (atLeast) keyBinding.matchesAtLeast(keyCode, scanCode, modifiers) else + keyBinding.matches(keyCode, scanCode, modifiers) + } } diff --git a/src/main/kotlin/events/WorldMouseMoveEvent.kt b/src/main/kotlin/events/WorldMouseMoveEvent.kt new file mode 100644 index 0000000..7a17ba4 --- /dev/null +++ b/src/main/kotlin/events/WorldMouseMoveEvent.kt @@ -0,0 +1,5 @@ +package moe.nea.firmament.events + +data class WorldMouseMoveEvent(val deltaX: Double, val deltaY: Double) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt index 57ff289..452bc56 100644 --- a/src/main/kotlin/features/macros/KeyComboTrie.kt +++ b/src/main/kotlin/features/macros/KeyComboTrie.kt @@ -44,6 +44,11 @@ sealed interface KeyComboTrie { } } +@Serializable +data class MacroWheel( + val key: SavedKeyBinding, + val options: List +) @Serializable data class ComboKeyAction( diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt index 78a5948..91de423 100644 --- a/src/main/kotlin/features/macros/MacroData.kt +++ b/src/main/kotlin/features/macros/MacroData.kt @@ -5,7 +5,8 @@ import moe.nea.firmament.util.data.DataHolder @Serializable data class MacroData( - var comboActions: List = listOf(), -){ + var comboActions: List = listOf(), + var wheels: List = listOf(), +) { object DConfig : DataHolder(kotlinx.serialization.serializer(), "macros", ::MacroData) } diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt index 17fdd0a..88f20a1 100644 --- a/src/main/kotlin/features/macros/MacroUI.kt +++ b/src/main/kotlin/features/macros/MacroUI.kt @@ -32,7 +32,142 @@ class MacroUI { @field:Bind("combos") val combos = Combos() - class Combos { + @field:Bind("wheels") + val wheels = Wheels() + var dontSave = false + + @Bind + fun beforeClose(): CloseEventListener.CloseAction { + if (!dontSave) + save() + return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE + } + + fun save() { + MacroData.DConfig.data.comboActions = combos.actions.map { it.asSaveable() } + MacroData.DConfig.data.wheels = wheels.wheels.map { it.asSaveable() } + MacroData.DConfig.markDirty() + RadialMacros.setWheels(MacroData.DConfig.data.wheels) + ComboProcessor.setActions(MacroData.DConfig.data.comboActions) + } + + fun discard() { + dontSave = true + MC.screen?.close() + } + + class Command( + @field:Bind("text") + var text: String, + val parent: Wheel, + ) { + @Bind + fun delete() { + parent.editableCommands.removeIf { it === this } + parent.editableCommands.update() + parent.commands.update() + } + + fun asCommandAction() = CommandAction(text) + } + + inner class Wheel( + val parent: Wheels, + var binding: SavedKeyBinding, + commands: List, + ) { + + fun asSaveable(): MacroWheel { + return MacroWheel(binding, commands.map { it.asCommandAction() }) + } + + @Bind("keyCombo") + fun text() = binding.format().string + + @field:Bind("commands") + val commands = commands.mapTo(ObservableList(mutableListOf())) { Command(it.command, this) } + + @field:Bind("editableCommands") + val editableCommands = this.commands.toObservableList() + + @Bind + fun addOption() { + editableCommands.add(Command("", this)) + } + + @Bind + fun back() { + MC.screen?.close() + } + + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_wheel", this, MC.screen) + } + + @Bind + fun delete() { + parent.wheels.removeIf { it === this } + parent.wheels.update() + } + + val sm = KeyBindingStateManager( + { binding }, + { binding = it }, + ::blur, + ::requestFocus + ) + + @field:Bind + val button = sm.createButton() + + init { + sm.updateLabel() + } + + fun blur() { + button.blur() + } + + + fun requestFocus() { + button.requestFocus() + } + } + + inner class Wheels { + @field:Bind("wheels") + val wheels: ObservableList = MacroData.DConfig.data.wheels.mapTo(ObservableList(mutableListOf())) { + Wheel(this, it.key, it.options.map { CommandAction((it as CommandAction).command) }) + } + + @Bind + fun discard() { + this@MacroUI.discard() + } + + @Bind + fun saveAndClose() { + this@MacroUI.saveAndClose() + } + + @Bind + fun save() { + this@MacroUI.save() + } + + @Bind + fun addWheel() { + wheels.add(Wheel(this, SavedKeyBinding.unbound(), listOf())) + } + } + + fun saveAndClose() { + save() + MC.screen?.close() + } + + inner class Combos { @field:Bind("actions") val actions: ObservableList = ObservableList( MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) { @@ -40,15 +175,6 @@ class MacroUI { } ) - var dontSave = false - - @Bind - fun beforeClose(): CloseEventListener.CloseAction { - if (!dontSave) - save() - return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE - } - @Bind fun addCommand() { actions.add( @@ -64,21 +190,17 @@ class MacroUI { @Bind fun discard() { - dontSave = true - MC.screen?.close() + this@MacroUI.discard() } @Bind fun saveAndClose() { - save() - MC.screen?.close() + this@MacroUI.discard() } @Bind fun save() { - MacroData.DConfig.data.comboActions = actions.map { it.asSaveable() } - MacroData.DConfig.markDirty() - ComboProcessor.setActions(MacroData.DConfig.data.comboActions) // TODO: automatically reload those from the config on startup + this@MacroUI.save() } } @@ -101,18 +223,19 @@ class MacroUI { button.blur() } + + fun requestFocus() { + button.requestFocus() + } + @Bind fun delete() { parent.combo.removeIf { it === this } parent.combo.update() } - - fun requestFocus() { - button.requestFocus() - } } - class ActionEditor(val action: ComboKeyAction, val parent: MacroUI.Combos) { + class ActionEditor(val action: ComboKeyAction, val parent: Combos) { fun asSaveable(): ComboKeyAction { return ComboKeyAction( CommandAction(command), @@ -145,9 +268,10 @@ class MacroUI { parent.actions.removeIf { it === this } parent.actions.update() } + @Bind fun edit() { - MC.screen = MoulConfigUtils.loadScreen("config/macros/editor", this, MC.screen) + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_combo", this, MC.screen) } } } diff --git a/src/main/kotlin/features/macros/RadialMenu.kt b/src/main/kotlin/features/macros/RadialMenu.kt new file mode 100644 index 0000000..2e09c44 --- /dev/null +++ b/src/main/kotlin/features/macros/RadialMenu.kt @@ -0,0 +1,149 @@ +package moe.nea.firmament.features.macros + +import org.joml.Vector2f +import util.render.CustomRenderLayers +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt +import net.minecraft.client.gui.DrawContext +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldMouseMoveEvent +import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenu +import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenuOption +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.RenderCircleProgress +import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.render.lerpAngle +import moe.nea.firmament.util.render.wrapAngle +import moe.nea.firmament.util.render.τ + +object RadialMenuViewer { + interface RadialMenu { + val key: SavedKeyBinding + val options: List + } + + interface RadialMenuOption { + val isEnabled: Boolean + fun resolve() + fun renderSlice(drawContext: DrawContext) + } + + var activeMenu: RadialMenu? = null + set(value) { + field = value + delta = Vector2f(0F, 0F) + } + var delta = Vector2f(0F, 0F) + val maxSelectionSize = 100F + + @Subscribe + fun onMouseMotion(event: WorldMouseMoveEvent) { + val menu = activeMenu ?: return + event.cancel() + delta.add(event.deltaX.toFloat(), event.deltaY.toFloat()) + val m = delta.lengthSquared() + if (m > maxSelectionSize * maxSelectionSize) { + delta.mul(maxSelectionSize / sqrt(m)) + } + } + + val INNER_CIRCLE_RADIUS = 16 + + @Subscribe + fun onRender(event: HudRenderEvent) { + val menu = activeMenu ?: return + val mat = event.context.matrices + mat.push() + mat.translate( + (MC.window.scaledWidth) / 2F, + (MC.window.scaledHeight) / 2F, + 0F + ) + val sliceWidth = (τ / menu.options.size).toFloat() + var selectedAngle = wrapAngle(atan2(delta.y, delta.x)) + if (delta.lengthSquared() < INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS) + selectedAngle = Float.NaN + for ((idx, option) in menu.options.withIndex()) { + val range = (sliceWidth * idx)..(sliceWidth * (idx + 1)) + mat.push() + mat.scale(64F, 64F, 1F) + val cutout = INNER_CIRCLE_RADIUS / 64F / 2 + RenderCircleProgress.renderCircularSlice( + event.context, + CustomRenderLayers.TRANSLUCENT_CIRCLE_GUI, + 0F, 1F, 0F, 1F, + range, + color = if (selectedAngle in range) 0x70A0A0A0 else 0x70FFFFFF, + innerCutoutRadius = cutout + ) + mat.pop() + mat.push() + val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F) + val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F) + mat.translate(vec.x, vec.y, 0F) + option.renderSlice(event.context) + mat.pop() + } + event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), me.shedaniel.math.Color.ofOpaque(0x00FF00)) + mat.pop() + } + + @Subscribe + fun onTick(event: TickEvent) { + val menu = activeMenu ?: return + if (!menu.key.isPressed(true)) { + val angle = atan2(delta.y, delta.x) + + val choiceIndex = (wrapAngle(angle) * menu.options.size / τ).toInt() + val choice = menu.options[choiceIndex] + val selectedAny = delta.lengthSquared() > INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS + activeMenu = null + if (selectedAny) + choice.resolve() + } + } + +} + +object RadialMacros { + var wheels = MacroData.DConfig.data.wheels + private set + + fun setWheels(wheels: List) { + this.wheels = wheels + RadialMenuViewer.activeMenu = null + } + + @Subscribe + fun onOpen(event: WorldKeyboardEvent) { + if (RadialMenuViewer.activeMenu != null) return + wheels.forEach { wheel -> + if (event.matches(wheel.key, atLeast = true)) { + class R(val action: HotkeyAction) : RadialMenuOption { + override val isEnabled: Boolean + get() = true + + override fun resolve() { + action.execute() + } + + override fun renderSlice(drawContext: DrawContext) { + drawContext.drawCenteredTextWithShadow(MC.font, action.label, 0, 0, -1) + } + } + RadialMenuViewer.activeMenu = object : RadialMenu { + override val key: SavedKeyBinding + get() = wheel.key + override val options: List = + wheel.options.map { R(it) } + } + } + } + } +} diff --git a/src/main/kotlin/keybindings/IKeyBinding.kt b/src/main/kotlin/keybindings/IKeyBinding.kt index 1975361..9d9b106 100644 --- a/src/main/kotlin/keybindings/IKeyBinding.kt +++ b/src/main/kotlin/keybindings/IKeyBinding.kt @@ -6,24 +6,45 @@ import net.minecraft.client.option.KeyBinding interface IKeyBinding { fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean + fun matchesAtLeast(keyCode: Int, scanCode: Int, modifiers: Int): Boolean fun withModifiers(wantedModifiers: Int): IKeyBinding { val old = this return object : IKeyBinding { override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return old.matches(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers + return old.matchesAtLeast(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers } - } + + override fun matchesAtLeast( + keyCode: Int, + scanCode: Int, + modifiers: Int + ): Boolean { + return old.matchesAtLeast(keyCode, scanCode, modifiers) && (modifiers.inv() and wantedModifiers) == 0 + } + } } companion object { fun minecraft(keyBinding: KeyBinding) = object : IKeyBinding { override fun matches(keyCode: Int, scanCode: Int, modifiers: Int) = keyBinding.matchesKey(keyCode, scanCode) - } + + override fun matchesAtLeast( + keyCode: Int, + scanCode: Int, + modifiers: Int + ): Boolean = + keyBinding.matchesKey(keyCode, scanCode) + } fun ofKeyCode(wantedKeyCode: Int) = object : IKeyBinding { - override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode - } + override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode && modifiers == 0 + override fun matchesAtLeast( + keyCode: Int, + scanCode: Int, + modifiers: Int + ): Boolean = keyCode == wantedKeyCode + } } } diff --git a/src/main/kotlin/keybindings/SavedKeyBinding.kt b/src/main/kotlin/keybindings/SavedKeyBinding.kt index 3ae0b89..fc0270d 100644 --- a/src/main/kotlin/keybindings/SavedKeyBinding.kt +++ b/src/main/kotlin/keybindings/SavedKeyBinding.kt @@ -7,6 +7,7 @@ import net.minecraft.client.util.InputUtil import net.minecraft.text.Text import moe.nea.firmament.util.MC +// TODO: add support for mouse keybindings @Serializable data class SavedKeyBinding( val keyCode: Int, @@ -86,6 +87,12 @@ data class SavedKeyBinding( (shift == this.shift) } + override fun matchesAtLeast(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false + val (shift, ctrl, alt) = getMods(modifiers) + return keyCode == this.keyCode && this.shift <= shift && this.ctrl <= ctrl && this.alt <= alt + } + override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false return keyCode == this.keyCode && getMods(modifiers) == Triple(shift, ctrl, alt) diff --git a/src/main/kotlin/util/collections/RangeUtil.kt b/src/main/kotlin/util/collections/RangeUtil.kt new file mode 100644 index 0000000..a7029ac --- /dev/null +++ b/src/main/kotlin/util/collections/RangeUtil.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.util.collections + +import kotlin.math.floor + +val ClosedFloatingPointRange.centre get() = (endInclusive + start) / 2 + +fun ClosedFloatingPointRange.nonNegligibleSubSectionsAlignedWith( + interval: Float +): Iterable { + require(interval.isFinite()) + val range = this + return object : Iterable { + override fun iterator(): Iterator { + return object : FloatIterator() { + var polledValue: Float = range.start + var lastValue: Float = polledValue + + override fun nextFloat(): Float { + if (!hasNext()) throw NoSuchElementException() + lastValue = polledValue + polledValue = Float.NaN + return lastValue + } + + override fun hasNext(): Boolean { + if (!polledValue.isNaN()) { + return true + } + if (lastValue == range.endInclusive) + return false + polledValue = (floor(lastValue / interval) + 1) * interval + if (polledValue > range.endInclusive) { + polledValue = range.endInclusive + } + return true + } + } + } + } +} diff --git a/src/main/kotlin/util/math/Projections.kt b/src/main/kotlin/util/math/Projections.kt new file mode 100644 index 0000000..359b21b --- /dev/null +++ b/src/main/kotlin/util/math/Projections.kt @@ -0,0 +1,46 @@ +package moe.nea.firmament.util.math + +import kotlin.math.absoluteValue +import kotlin.math.cos +import kotlin.math.sin +import net.minecraft.util.math.Vec2f +import moe.nea.firmament.util.render.wrapAngle + +object Projections { + object Two { + val ε = 1e-6 + val π = moe.nea.firmament.util.render.π + val τ = 2 * π + + fun isNullish(float: Float) = float.absoluteValue < ε + + fun xInterceptOfLine(origin: Vec2f, direction: Vec2f): Vec2f? { + if (isNullish(direction.x)) + return Vec2f(origin.x, 0F) + if (isNullish(direction.y)) + return null + + val slope = direction.y / direction.x + return Vec2f(origin.x - origin.y / slope, 0F) + } + + fun interceptAlongCardinal(distanceFromAxis: Float, slope: Float): Float? { + if (isNullish(slope)) + return null + return -distanceFromAxis / slope + } + + fun projectAngleOntoUnitBox(angleRadians: Double): Vec2f { + val angleRadians = wrapAngle(angleRadians) + val cx = cos(angleRadians) + val cy = sin(angleRadians) + + val ex = 1 / cx.absoluteValue + val ey = 1 / cy.absoluteValue + + val e = minOf(ex, ey) + + return Vec2f((cx * e).toFloat(), (cy * e).toFloat()) + } + } +} diff --git a/src/main/kotlin/util/render/CustomRenderLayers.kt b/src/main/kotlin/util/render/CustomRenderLayers.kt index 7f3cdec..be0bbd7 100644 --- a/src/main/kotlin/util/render/CustomRenderLayers.kt +++ b/src/main/kotlin/util/render/CustomRenderLayers.kt @@ -1,10 +1,12 @@ package util.render +import com.mojang.blaze3d.pipeline.BlendFunction import com.mojang.blaze3d.pipeline.RenderPipeline import com.mojang.blaze3d.platform.DepthTestFunction import com.mojang.blaze3d.vertex.VertexFormat.DrawMode import java.util.function.Function import net.minecraft.client.gl.RenderPipelines +import net.minecraft.client.gl.UniformType import net.minecraft.client.render.RenderLayer import net.minecraft.client.render.RenderPhase import net.minecraft.client.render.VertexFormats @@ -38,23 +40,33 @@ object CustomRenderPipelines { .withCull(false) .withDepthWrite(false) .build() + + val CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS = + RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET) + .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES) + .withLocation(Firmament.identifier("gui_textured_overlay_tris_circle")) + .withUniform("InnerCutoutRadius", UniformType.FLOAT) + .withFragmentShader(Firmament.identifier("circle_discard_color")) + .withBlend(BlendFunction.TRANSLUCENT) + .build() } object CustomRenderLayers { - - inline fun memoizeTextured(crossinline func: (Identifier) -> RenderLayer) = memoize(func) inline fun memoize(crossinline func: (T) -> R): Function { return Util.memoize { it: T -> func(it) } } val GUI_TEXTURED_NO_DEPTH_TRIS = memoizeTextured { texture -> - RenderLayer.of("firmament_gui_textured_overlay_tris", - RenderLayer.DEFAULT_BUFFER_SIZE, - CustomRenderPipelines.GUI_TEXTURED_NO_DEPTH_TRIS, - RenderLayer.MultiPhaseParameters.builder().texture( - RenderPhase.Texture(texture, TriState.DEFAULT, false)) - .build(false)) + RenderLayer.of( + "firmament_gui_textured_overlay_tris", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.GUI_TEXTURED_NO_DEPTH_TRIS, + RenderLayer.MultiPhaseParameters.builder().texture( + RenderPhase.Texture(texture, TriState.DEFAULT, false) + ) + .build(false) + ) } val LINES = RenderLayer.of( "firmament_lines", @@ -71,4 +83,13 @@ object CustomRenderLayers { .lightmap(RenderPhase.DISABLE_LIGHTMAP) .build(false) ) + + val TRANSLUCENT_CIRCLE_GUI = + RenderLayer.of( + "firmament_circle_gui", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS, + RenderLayer.MultiPhaseParameters.builder() + .build(false) + ) } diff --git a/src/main/kotlin/util/render/DrawContextExt.kt b/src/main/kotlin/util/render/DrawContextExt.kt index fa92cd7..a833c86 100644 --- a/src/main/kotlin/util/render/DrawContextExt.kt +++ b/src/main/kotlin/util/render/DrawContextExt.kt @@ -1,18 +1,12 @@ package moe.nea.firmament.util.render -import com.mojang.blaze3d.pipeline.RenderPipeline -import com.mojang.blaze3d.platform.DepthTestFunction import com.mojang.blaze3d.systems.RenderSystem -import com.mojang.blaze3d.vertex.VertexFormat.DrawMode import me.shedaniel.math.Color import org.joml.Matrix4f import util.render.CustomRenderLayers -import net.minecraft.client.gl.RenderPipelines import net.minecraft.client.gui.DrawContext import net.minecraft.client.render.RenderLayer -import net.minecraft.client.render.VertexFormats import net.minecraft.util.Identifier -import moe.nea.firmament.Firmament import moe.nea.firmament.util.MC fun DrawContext.isUntranslatedGuiDrawContext(): Boolean { @@ -64,9 +58,10 @@ fun DrawContext.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Colo RenderSystem.lineWidth(MC.window.scaleFactor.toFloat()) draw { vertexConsumers -> val buf = vertexConsumers.getBuffer(CustomRenderLayers.LINES) - buf.vertex(fromX.toFloat(), fromY.toFloat(), 0F).color(color.color) + val matrix = this.matrices.peek() + buf.vertex(matrix, fromX.toFloat(), fromY.toFloat(), 0F).color(color.color) .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F) - buf.vertex(toX.toFloat(), toY.toFloat(), 0F).color(color.color) + buf.vertex(matrix, toX.toFloat(), toY.toFloat(), 0F).color(color.color) .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F) } } diff --git a/src/main/kotlin/util/render/LerpUtils.kt b/src/main/kotlin/util/render/LerpUtils.kt index f2c2f25..63a13ec 100644 --- a/src/main/kotlin/util/render/LerpUtils.kt +++ b/src/main/kotlin/util/render/LerpUtils.kt @@ -1,33 +1,36 @@ - package moe.nea.firmament.util.render import me.shedaniel.math.Color -val pi = Math.PI -val tau = Math.PI * 2 -fun lerpAngle(a: Float, b: Float, progress: Float): Float { - // TODO: there is at least 10 mods to many in here lol - val shortestAngle = ((((b.mod(tau) - a.mod(tau)).mod(tau)) + tau + pi).mod(tau)) - pi - return ((a + (shortestAngle) * progress).mod(tau)).toFloat() +val π = Math.PI +val τ = Math.PI * 2 +fun lerpAngle(a: Float, b: Float, progress: Float): Float { + // TODO: there is at least 10 mods to many in here lol + val shortestAngle = ((((b.mod(τ) - a.mod(τ)).mod(τ)) + τ + π).mod(τ)) - π + return ((a + (shortestAngle) * progress).mod(τ)).toFloat() } +fun wrapAngle(angle: Float): Float = (angle.mod(τ) + τ).mod(τ).toFloat() +fun wrapAngle(angle: Double): Double = (angle.mod(τ) + τ).mod(τ) + fun lerp(a: Float, b: Float, progress: Float): Float { - return a + (b - a) * progress + return a + (b - a) * progress } + fun lerp(a: Int, b: Int, progress: Float): Int { - return (a + (b - a) * progress).toInt() + return (a + (b - a) * progress).toInt() } fun ilerp(a: Float, b: Float, value: Float): Float { - return (value - a) / (b - a) + return (value - a) / (b - a) } fun lerp(a: Color, b: Color, progress: Float): Color { - return Color.ofRGBA( - lerp(a.red, b.red, progress), - lerp(a.green, b.green, progress), - lerp(a.blue, b.blue, progress), - lerp(a.alpha, b.alpha, progress), - ) + return Color.ofRGBA( + lerp(a.red, b.red, progress), + lerp(a.green, b.green, progress), + lerp(a.blue, b.blue, progress), + lerp(a.alpha, b.alpha, progress), + ) } diff --git a/src/main/kotlin/util/render/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt index d759033..81dde6f 100644 --- a/src/main/kotlin/util/render/RenderCircleProgress.kt +++ b/src/main/kotlin/util/render/RenderCircleProgress.kt @@ -1,16 +1,87 @@ package moe.nea.firmament.util.render +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.vertex.VertexFormat import io.github.notenoughupdates.moulconfig.platform.next +import java.util.OptionalInt import org.joml.Matrix4f -import org.joml.Vector2f import util.render.CustomRenderLayers -import kotlin.math.atan2 -import kotlin.math.tan import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.BufferBuilder +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.util.BufferAllocator import net.minecraft.util.Identifier +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.collections.nonNegligibleSubSectionsAlignedWith +import moe.nea.firmament.util.math.Projections object RenderCircleProgress { + fun renderCircularSlice( + drawContext: DrawContext, + layer: RenderLayer, + u1: Float, + u2: Float, + v1: Float, + v2: Float, + angleRadians: ClosedFloatingPointRange, + color: Int = -1, + innerCutoutRadius: Float = 0F + ) { + drawContext.draw() + val sections = angleRadians.nonNegligibleSubSectionsAlignedWith((τ / 8f).toFloat()) + .zipWithNext().toList() + BufferAllocator(layer.vertexFormat.vertexSize * sections.size * 3).use { allocator -> + + val bufferBuilder = BufferBuilder(allocator, VertexFormat.DrawMode.TRIANGLES, layer.vertexFormat) + val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix + + for ((sectionStart, sectionEnd) in sections) { + val firstPoint = Projections.Two.projectAngleOntoUnitBox(sectionStart.toDouble()) + val secondPoint = Projections.Two.projectAngleOntoUnitBox(sectionEnd.toDouble()) + fun ilerp(f: Float): Float = + ilerp(-1f, 1f, f) + + bufferBuilder + .vertex(matrix, secondPoint.x, secondPoint.y, 0F) + .texture(lerp(u1, u2, ilerp(secondPoint.x)), lerp(v1, v2, ilerp(secondPoint.y))) + .color(color) + .next() + bufferBuilder + .vertex(matrix, firstPoint.x, firstPoint.y, 0F) + .texture(lerp(u1, u2, ilerp(firstPoint.x)), lerp(v1, v2, ilerp(firstPoint.y))) + .color(color) + .next() + bufferBuilder + .vertex(matrix, 0F, 0F, 0F) + .texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F))) + .color(color) + .next() + } + + bufferBuilder.end().use { buffer -> + // TODO: write a better utility to pass uniforms :sob: ill even take a mixin at this point + if (innerCutoutRadius <= 0) { + layer.draw(buffer) + return + } + val vertexBuffer = layer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer) + val indexBufferConstructor = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.TRIANGLES) + val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount) + RenderSystem.getDevice().createCommandEncoder().createRenderPass( + MC.instance.framebuffer.colorAttachment, + OptionalInt.empty(), + ).use { renderPass -> + renderPass.setPipeline(layer.pipeline) + renderPass.setUniform("InnerCutoutRadius", innerCutoutRadius) + renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType) + renderPass.setVertexBuffer(0, vertexBuffer) + renderPass.drawIndexed(0, buffer.drawParameters.indexCount) + } + } + } + } + fun renderCircle( drawContext: DrawContext, texture: Identifier, @@ -20,66 +91,11 @@ object RenderCircleProgress { v1: Float, v2: Float, ) { - drawContext.draw { - val bufferBuilder = it.getBuffer(CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture)) - val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix - - val corners = listOf( - Vector2f(0F, -1F), - Vector2f(1F, -1F), - Vector2f(1F, 0F), - Vector2f(1F, 1F), - Vector2f(0F, 1F), - Vector2f(-1F, 1F), - Vector2f(-1F, 0F), - Vector2f(-1F, -1F), - ) - - for (i in (0 until 8)) { - if (progress < i / 8F) { - break - } - val second = corners[(i + 1) % 8] - val first = corners[i] - if (progress <= (i + 1) / 8F) { - val internalProgress = 1 - (progress - i / 8F) * 8F - val angle = lerpAngle( - atan2(second.y, second.x), - atan2(first.y, first.x), - internalProgress - ) - if (angle < tau / 8 || angle >= tau * 7 / 8) { - second.set(1F, tan(angle)) - } else if (angle < tau * 3 / 8) { - second.set(1 / tan(angle), 1F) - } else if (angle < tau * 5 / 8) { - second.set(-1F, -tan(angle)) - } else { - second.set(-1 / tan(angle), -1F) - } - } - - fun ilerp(f: Float): Float = - ilerp(-1f, 1f, f) - - bufferBuilder - .vertex(matrix, second.x, second.y, 0F) - .texture(lerp(u1, u2, ilerp(second.x)), lerp(v1, v2, ilerp(second.y))) - .color(-1) - .next() - bufferBuilder - .vertex(matrix, first.x, first.y, 0F) - .texture(lerp(u1, u2, ilerp(first.x)), lerp(v1, v2, ilerp(first.y))) - .color(-1) - .next() - bufferBuilder - .vertex(matrix, 0F, 0F, 0F) - .texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F))) - .color(-1) - .next() - } - } + renderCircularSlice( + drawContext, + CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture), + u1, u2, v1, v2, + (-τ / 4).toFloat()..(progress * τ - τ / 4).toFloat() + ) } - - } diff --git a/src/main/resources/assets/firmament/gui/config/macros/combos.xml b/src/main/resources/assets/firmament/gui/config/macros/combos.xml index b2865ab..5141125 100644 --- a/src/main/resources/assets/firmament/gui/config/macros/combos.xml +++ b/src/main/resources/assets/firmament/gui/config/macros/combos.xml @@ -3,7 +3,6 @@ > - diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor.xml b/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml similarity index 100% rename from src/main/resources/assets/firmament/gui/config/macros/editor.xml rename to src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml b/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml new file mode 100644 index 0000000..e4dc2b4 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml @@ -0,0 +1,43 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/main/resources/assets/firmament/gui/config/macros/index.xml b/src/main/resources/assets/firmament/gui/config/macros/index.xml index 3fa9f99..f6a1545 100644 --- a/src/main/resources/assets/firmament/gui/config/macros/index.xml +++ b/src/main/resources/assets/firmament/gui/config/macros/index.xml @@ -1,16 +1,27 @@ +>
- - - - - - - - - - + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/resources/assets/firmament/gui/config/macros/wheel.xml b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml new file mode 100644 index 0000000..19922fe --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh new file mode 100644 index 0000000..ae46059 --- /dev/null +++ b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh @@ -0,0 +1,22 @@ +#version 150 + +in vec4 vertexColor; +in vec2 texCoord0; + +uniform vec4 ColorModulator; +uniform float InnerCutoutRadius; + +out vec4 fragColor; + +void main() { + vec4 color = vertexColor; + if (color.a == 0.0) { + discard; + } + float d = length(texCoord0 - vec2(0.5)); + if (d > 0.5 || d < InnerCutoutRadius) + { + discard; + } + fragColor = color * ColorModulator; +} diff --git a/src/test/kotlin/util/math/ProjectionsBoxTest.kt b/src/test/kotlin/util/math/ProjectionsBoxTest.kt new file mode 100644 index 0000000..04720a3 --- /dev/null +++ b/src/test/kotlin/util/math/ProjectionsBoxTest.kt @@ -0,0 +1,28 @@ +package moe.nea.firmament.test.util.math + +import java.util.stream.Stream +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import kotlin.streams.asStream +import net.minecraft.util.math.Vec2f +import moe.nea.firmament.util.math.Projections + +class ProjectionsBoxTest { + val Double.degrees get() = Math.toRadians(this) + + @TestFactory + fun testProjections(): Stream { + return sequenceOf( + 0.0.degrees to Vec2f(1F, 0F), + 63.4349.degrees to Vec2f(0.5F, 1F), + ).map { (angle, expected) -> + DynamicTest.dynamicTest("ProjectionsBoxTest::projectAngleOntoUnitBox(${angle})") { + val actual = Projections.Two.projectAngleOntoUnitBox(angle) + fun msg() = "Expected (${expected.x}, ${expected.y}) got (${actual.x}, ${actual.y})" + Assertions.assertEquals(expected.x, actual.x, 0.0001F, ::msg) + Assertions.assertEquals(expected.y, actual.y, 0.0001F, ::msg) + } + }.asStream() + } +}