feat: Add macro wheels

This commit is contained in:
Linnea Gräf
2025-06-17 20:59:45 +02:00
parent 775933d516
commit 4b9e966ca7
24 changed files with 753 additions and 147 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -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();
}
}

View File

@@ -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<WorldKeyboardEvent>()
companion object : FirmamentEventBus<WorldKeyboardEvent>()
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)
}
}

View File

@@ -0,0 +1,5 @@
package moe.nea.firmament.events
data class WorldMouseMoveEvent(val deltaX: Double, val deltaY: Double) : FirmamentEvent.Cancellable() {
companion object : FirmamentEventBus<WorldMouseMoveEvent>()
}

View File

@@ -44,6 +44,11 @@ sealed interface KeyComboTrie {
}
}
@Serializable
data class MacroWheel(
val key: SavedKeyBinding,
val options: List<HotkeyAction>
)
@Serializable
data class ComboKeyAction(

View File

@@ -5,7 +5,8 @@ import moe.nea.firmament.util.data.DataHolder
@Serializable
data class MacroData(
var comboActions: List<ComboKeyAction> = listOf(),
){
var comboActions: List<ComboKeyAction> = listOf(),
var wheels: List<MacroWheel> = listOf(),
) {
object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData)
}

View File

@@ -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<CommandAction>,
) {
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<Wheel> = 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<ActionEditor> = 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)
}
}
}

View File

@@ -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<RadialMenuOption>
}
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<MacroWheel>) {
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<RadialMenuOption> =
wheel.options.map { R(it) }
}
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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)

View File

@@ -0,0 +1,40 @@
package moe.nea.firmament.util.collections
import kotlin.math.floor
val ClosedFloatingPointRange<Float>.centre get() = (endInclusive + start) / 2
fun ClosedFloatingPointRange<Float>.nonNegligibleSubSectionsAlignedWith(
interval: Float
): Iterable<Float> {
require(interval.isFinite())
val range = this
return object : Iterable<Float> {
override fun iterator(): Iterator<Float> {
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
}
}
}
}
}

View File

@@ -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())
}
}
}

View File

@@ -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 <T, R> memoize(crossinline func: (T) -> R): Function<T, R> {
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)
)
}

View File

@@ -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)
}
}

View File

@@ -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),
)
}

View File

@@ -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<Float>,
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()
)
}
}

View File

@@ -3,7 +3,6 @@
>
<Panel background="TRANSPARENT" insets="10">
<Column>
<Meta beforeClose="@beforeClose"/>
<ScrollPanel width="380" height="300">
<Align horizontal="CENTER">
<Array data="@actions">

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
>
<Center>
<Panel background="VANILLA" insets="10">
<Column>
<Row>
<firm:Button onClick="@back">
<Text text="←"/>
</firm:Button>
<Text text="Editing wheel macro"/>
</Row>
<Row>
<Text text="Key (Hold):"/>
<Align horizontal="RIGHT">
<firm:Fixed width="160">
<Indirect value="@button"/>
</firm:Fixed>
</Align>
</Row>
<Row>
<Text text="Menu Options:"/>
<Align horizontal="RIGHT">
<firm:Button onClick="@addOption">
<Text text="+"/>
</firm:Button>
</Align>
</Row>
<Array data="@editableCommands">
<Row>
<Text text="/"/>
<TextField value="@text" width="160"/>
<Align horizontal="RIGHT">
<firm:Button onClick="@delete">
<Text text="Delete"/>
</firm:Button>
</Align>
</Row>
</Array>
</Column>
</Panel>
</Center>
</Root>

View File

@@ -1,16 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Root xmlns="http://notenoughupdates.org/moulconfig"
xmlns:firm="http://firmament.nea.moe/moulconfig">
>
<Center>
<Tabs>
<Tab>
<Tab.Header>
<Text text="Combo Macros"/>
</Tab.Header>
<Tab.Body>
<Fragment value="firmament:gui/config/macros/combos.xml" bind="@combos"/>
</Tab.Body>
</Tab>
</Tabs>
<Row>
<Tabs>
<Tab>
<Tab.Header>
<Text text="Combo Macros"/>
</Tab.Header>
<Tab.Body>
<Fragment value="firmament:gui/config/macros/combos.xml" bind="@combos"/>
</Tab.Body>
</Tab>
<Tab>
<Tab.Header>
<Text text="Macro Wheel"/>
</Tab.Header>
<Tab.Body>
<Fragment value="firmament:gui/config/macros/wheel.xml" bind="@wheels"/>
</Tab.Body>
</Tab>
</Tabs>
<Meta beforeClose="@beforeClose"/>
</Row>
</Center>
</Root>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
>
<Panel background="TRANSPARENT" insets="10">
<Column>
<ScrollPanel width="380" height="300">
<Align horizontal="CENTER">
<Array data="@wheels">
<Panel background="TRANSPARENT" insets="3">
<Panel background="VANILLA" insets="6">
<Column>
<Row>
<Text text="@keyCombo" width="250"/>
<Align horizontal="RIGHT">
<Row>
<firm:Button onClick="@edit">
<Text text="Edit"/>
</firm:Button>
<Spacer width="12"/>
<firm:Button onClick="@delete">
<Text text="Delete"/>
</firm:Button>
</Row>
</Align>
</Row>
<Array data="@commands">
<Text text="@text" width="280"/>
</Array>
</Column>
</Panel>
</Panel>
</Array>
</Align>
</ScrollPanel>
<Align horizontal="RIGHT">
<Row>
<firm:Button onClick="@discard">
<Text text="Discard Changes"/>
</firm:Button>
<firm:Button onClick="@saveAndClose">
<Text text="Save &amp; Close"/>
</firm:Button>
<firm:Button onClick="@save">
<Text text="Save"/>
</firm:Button>
<firm:Button onClick="@addWheel">
<Text text="Add Wheel"/>
</firm:Button>
</Row>
</Align>
</Column>
</Panel>
</Root>

View File

@@ -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;
}

View File

@@ -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<DynamicTest> {
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()
}
}