Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend: Graph Editor Directional Edge Support #2970

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class GraphConfig {

@Expose
@ConfigOption(name = "Toggle Ghost Position", desc = "Creates or removes the Ghost Position. This helps editing nodes tht are in the air.")
@ConfigEditorKeybind(defaultKey = Keyboard.KEY_F)
@ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE)
public int toggleGhostPosition = Keyboard.KEY_NONE;

@Expose
Expand Down Expand Up @@ -97,6 +97,11 @@ public class GraphConfig {
@ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE)
public int dissolveKey = Keyboard.KEY_NONE;

@Expose
@ConfigOption(name = "Edge Cycle", desc = "Cycles the direction of the edge.")
@ConfigEditorKeybind(defaultKey = Keyboard.KEY_H)
public int edgeCycle = Keyboard.KEY_H;

@Expose
@ConfigLink(owner = GraphConfig.class, field = "enabled")
public Position infoDisplay = new Position(20, 20);
Expand Down
208 changes: 160 additions & 48 deletions src/main/java/at/hannibal2/skyhanni/test/graph/GraphEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import at.hannibal2.skyhanni.utils.OSUtils
import at.hannibal2.skyhanni.utils.RaycastUtils
import at.hannibal2.skyhanni.utils.RenderUtils.draw3DLineNea
import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText
import at.hannibal2.skyhanni.utils.RenderUtils.drawPyramid
import at.hannibal2.skyhanni.utils.RenderUtils.drawWaypointFilled
import at.hannibal2.skyhanni.utils.RenderUtils.renderStrings
import kotlinx.coroutines.runBlocking
Expand All @@ -36,6 +37,7 @@ import net.minecraftforge.fml.common.eventhandler.EventPriority
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import java.awt.Color
import kotlin.math.min

@SkyHanniModule
object GraphEditor {
Expand Down Expand Up @@ -140,7 +142,10 @@ object GraphEditor {
if (activeNode != null) {
add("§eText: §6${KeyboardManager.getKeyName(config.textKey)}")
if (dissolvePossible) add("§eDissolve: §6${KeyboardManager.getKeyName(config.dissolveKey)}")
if (selectedEdge != null) add("§eSplit: §6${KeyboardManager.getKeyName(config.splitKey)}")
if (selectedEdge != null) {
add("§eSplit: §6${KeyboardManager.getKeyName(config.splitKey)}")
add("§eCycle Direction: §6${KeyboardManager.getKeyName(config.edgeCycle)}")
}
}
}

Expand Down Expand Up @@ -231,17 +236,49 @@ object GraphEditor {

private fun LorenzRenderWorldEvent.drawEdge(edge: GraphingEdge) {
if (edge.node1.position.distanceToPlayer() > config.maxNodeDistance) return
val color = when {
selectedEdge == edge -> edgeSelectedColor
edge in highlightedEdges -> edgeDijkstraColor
else -> edgeColor
}

this.draw3DLineNea(
edge.node1.position.add(0.5, 0.5, 0.5),
edge.node2.position.add(0.5, 0.5, 0.5),
when {
selectedEdge == edge -> edgeSelectedColor
edge in highlightedEdges -> edgeDijkstraColor
else -> edgeColor
},
color,
7,
!seeThroughBlocks,
)
if (edge.direction != EdgeDirection.BOTH) {
drawDirection(edge, color)
}
}

private fun LorenzRenderWorldEvent.drawDirection(edge: GraphingEdge, color: Color) {
val lineVec = edge.node2.position - edge.node1.position
val center = edge.node1.position + lineVec / 2.0
val quad1 = edge.node1.position + lineVec / 4.0
val quad2 = edge.node1.position + lineVec * (3.0 / 4.0)

val pyramidSize = lineVec.normalize()
.times(min(lineVec.length() / 10.0, 1.0)) * (if (edge.direction == EdgeDirection.ONE_TO_TWO) 1.0 else -1.0)

val lineOffsetVec = LorenzVec(0.5, 0.5, 0.5)

fun pyramidDraw(
pos: LorenzVec,
) {
this.drawPyramid(
pos + lineOffsetVec + pyramidSize,
pos + lineOffsetVec,
pos.crossProduct(lineVec).normalize().times(pyramidSize.length() / 2.5) + pos + lineOffsetVec,
color,
)
}

pyramidDraw(center)
pyramidDraw(quad1)
pyramidDraw(quad2)
}

private fun GraphingNode.getNodeColor() = when (this) {
Expand All @@ -262,7 +299,7 @@ object GraphEditor {

private fun chatAtDisable() = ChatUtils.clickableChat(
"Graph Editor is now inactive. §lClick to activate.",
GraphEditor::commandIn
GraphEditor::commandIn,
)

private fun input() {
Expand Down Expand Up @@ -398,16 +435,22 @@ object GraphEditor {
inTutorialMode = !inTutorialMode
ChatUtils.chat("Tutorial mode is now ${if (inTutorialMode) "active" else "inactive"}.")
}
if (selectedEdge != null && config.splitKey.isKeyClicked()) {
val edge = selectedEdge ?: return
feedBackInTutorial("Split Edge into a Node and two edges.")
val middle = edge.node1.position.middle(edge.node2.position).roundLocationToBlock()
val node = GraphingNode(id++, middle)
nodes.add(node)
edges.remove(edge)
addEdge(node, edge.node1)
addEdge(node, edge.node2)
activeNode = node
val selectedEdge = selectedEdge
if (selectedEdge != null) {
if (config.splitKey.isKeyClicked()) {
feedBackInTutorial("Split Edge into a Node and two edges.")
val middle = selectedEdge.node1.position.middle(selectedEdge.node2.position).roundLocationToBlock()
val node = GraphingNode(id++, middle)
nodes.add(node)
edges.remove(selectedEdge)
addEdge(selectedEdge.node1, node, selectedEdge.direction)
addEdge(node, selectedEdge.node2, selectedEdge.direction)
activeNode = node
}
if (config.edgeCycle.isKeyClicked()) {
selectedEdge.cycleDirection(activeNode)
feedBackInTutorial("Cycled Direction to: ${selectedEdge.cycleText(activeNode)}")
}
}
if (dissolvePossible && config.dissolveKey.isKeyClicked()) {
feedBackInTutorial("Dissolved the node, now it is gone.")
Expand All @@ -416,10 +459,24 @@ object GraphEditor {
val edge2 = edgePair[1]
val neighbors1 = if (edge1.node1 == activeNode) edge1.node2 else edge1.node1
val neighbors2 = if (edge2.node1 == activeNode) edge2.node2 else edge2.node1
val direction =
if (edge1.direction == EdgeDirection.BOTH || edge2.direction == EdgeDirection.BOTH) EdgeDirection.BOTH else when {
edge1.isValidConnectionFromTo(neighbors1, activeNode) && edge2.isValidConnectionFromTo(
activeNode,
neighbors2,
) -> EdgeDirection.ONE_TO_TWO

edge1.isValidConnectionFromTo(activeNode, neighbors1) && edge2.isValidConnectionFromTo(
neighbors2,
activeNode,
) -> EdgeDirection.TOW_TO_ONE

else -> EdgeDirection.BOTH
}
edges.removeAll(edgePair)
nodes.remove(activeNode)
activeNode = null
addEdge(neighbors1, neighbors2)
addEdge(neighbors1, neighbors2, direction)
}
}

Expand Down Expand Up @@ -518,18 +575,17 @@ object GraphEditor {
).let { e -> edges.indexOfFirst { it == e }.takeIf { it != -1 } }
else null

private fun addEdge(node1: GraphingNode?, node2: GraphingNode?) = if (node1 != null && node2 != null && node1 != node2) {
val edge = GraphingEdge(node1, node2)
if (edge.isInEdge(activeNode)) {
checkDissolve()
selectedEdge = findEdgeBetweenActiveAndClosest()
}
edges.add(edge)
} else false
private fun addEdge(node1: GraphingNode?, node2: GraphingNode?, direction: EdgeDirection = EdgeDirection.BOTH) =
if (node1 != null && node2 != null && node1 != node2) {
val edge = GraphingEdge(node1, node2, direction)
if (edge.isInEdge(activeNode)) {
checkDissolve()
selectedEdge = findEdgeBetweenActiveAndClosest()
}
edges.add(edge)
} else false

/** Has a side effect on the graphing graph, since it runs [prune] on the graphing graph*/
private fun compileGraph(): Graph {
prune()
val indexedTable = nodes.mapIndexed { index, node -> node.id to index }.toMap()
val nodes = nodes.mapIndexed { index, node ->
GraphNode(
Expand All @@ -538,11 +594,11 @@ object GraphEditor {
node.name,
node.tags.map {
it.internalName
}
},
)
}
val neighbours = GraphEditor.nodes.map { node ->
edges.filter { it.isInEdge(node) }.map { edge ->
edges.filter { it.isInEdge(node) && it.isValidDirectionFrom(node) }.map { edge ->
val otherNode =
if (node == edge.node1) edge.node2
else edge.node1
Expand All @@ -568,13 +624,26 @@ object GraphEditor {
},
)
val translation = graph.mapIndexed { index, node -> node to nodes[index] }.toMap()
edges.addAll(
graph.map { node ->
// TODO: Fix this to not use bang bangs
@Suppress("MapGetWithNotNullAssertionOperator")
node.neighbours.map { GraphingEdge(translation[node]!!, translation[it.key]!!) }
}.flatten().distinct(),

val neighbors = graph.map { node ->
// TODO: Fix this to not use bang bangs
@Suppress("MapGetWithNotNullAssertionOperator")
node.neighbours.map { GraphingEdge(translation[node]!!, translation[it.key]!!, EdgeDirection.ONE_TO_TWO) }
}.flatten()

val reduced = neighbors.groupingBy { it }.reduce(
{ _, accumulator, element ->
if (
(element.node1 == accumulator.node1 && accumulator.direction != element.direction) ||
(element.node1 == accumulator.node2 && accumulator.direction == element.direction)
) {
accumulator.direction = EdgeDirection.BOTH
}
accumulator
},
)

edges.addAll(reduced.values)
id = nodes.lastOrNull()?.id?.plus(1) ?: 0
checkDissolve()
selectedEdge = findEdgeBetweenActiveAndClosest()
Expand All @@ -598,11 +667,17 @@ object GraphEditor {

val path = GraphUtils.findShortestPathAsGraph(current, goal)

if (path.isEmpty()) {
ChatUtils.chat("No Path found")
}

val inGraph = path.map { nodes[it.id] }
highlightedNodes.addAll(inGraph)

val edge = edges.filter { highlightedNodes.contains(it.node1) && highlightedNodes.contains(it.node2) }
highlightedEdges.addAll(edge)
highlightedEdges.addAll(
highlightedNodes.zipWithNext { a, b -> edges.firstOrNull { it.isValidConnectionFromTo(a, b) } }
.filterNotNull(),
)
}

private fun clear() {
Expand All @@ -615,15 +690,6 @@ object GraphEditor {
ghostPosition = null
}

private fun prune() { // TODO fix
val hasNeighbours = nodes.associateWith { false }.toMutableMap()
edges.forEach {
hasNeighbours[it.node1] = true
hasNeighbours[it.node2] = true
}
nodes.removeIf { hasNeighbours[it] == false }
}

fun LorenzVec.distanceSqToPlayer(): Double = ghostPosition?.let { distanceSq(it) } ?: distanceSq(playerLocation())
}

Expand Down Expand Up @@ -651,7 +717,7 @@ class GraphingNode(
}
}

private class GraphingEdge(val node1: GraphingNode, val node2: GraphingNode) {
private class GraphingEdge(val node1: GraphingNode, val node2: GraphingNode, var direction: EdgeDirection = EdgeDirection.BOTH) {

fun isInEdge(node: GraphingNode?) = node1 == node || node2 == node

Expand All @@ -678,4 +744,50 @@ private class GraphingEdge(val node1: GraphingNode, val node2: GraphingNode) {
}
return result
}

fun cycleDirection(standpoint: GraphingNode?) {
direction = if (standpoint != node2) {
when (direction) {
EdgeDirection.BOTH -> EdgeDirection.ONE_TO_TWO
EdgeDirection.ONE_TO_TWO -> EdgeDirection.TOW_TO_ONE
EdgeDirection.TOW_TO_ONE -> EdgeDirection.BOTH
}
} else {
when (direction) {
EdgeDirection.BOTH -> EdgeDirection.TOW_TO_ONE
EdgeDirection.TOW_TO_ONE -> EdgeDirection.ONE_TO_TWO
EdgeDirection.ONE_TO_TWO -> EdgeDirection.BOTH
}
}
}

fun cycleText(standpoint: GraphingNode?) = when (direction) {
EdgeDirection.BOTH -> "Bidirectional"
EdgeDirection.ONE_TO_TWO -> if (standpoint != node1) {
"AwayFromYou"
} else {
"ToYou"
}

EdgeDirection.TOW_TO_ONE -> if (standpoint != node1) {
"ToYou"
} else {
"AwayFromYou"
}
}

fun isValidDirectionFrom(standpoint: GraphingNode?) = when (direction) {
EdgeDirection.BOTH -> true
EdgeDirection.ONE_TO_TWO -> standpoint == node1
EdgeDirection.TOW_TO_ONE -> standpoint == node2
}

fun isValidConnectionFromTo(a: GraphingNode?, b: GraphingNode?): Boolean =
((this.node1 == a && this.node2 == b) || (this.node1 == b && this.node2 == a)) && isValidDirectionFrom(a)
}

private enum class EdgeDirection {
BOTH,
ONE_TO_TWO,
TOW_TO_ONE,
}
8 changes: 8 additions & 0 deletions src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ data class LorenzVec(

fun angleInDeg(other: LorenzVec) = Math.toDegrees(angleInRad(other))

fun crossProduct(other: LorenzVec): LorenzVec = LorenzVec(
this.y * other.z - this.z * other.y,
this.z * other.x - this.x * other.z,
this.x * other.y - this.y * other.x,
)

fun scaledTo(other: LorenzVec) = this.normalize().times(other.length())

fun normalize() = length().let { LorenzVec(x / it, y / it, z / it) }

fun inverse() = LorenzVec(1.0 / x, 1.0 / y, 1.0 / z)
Expand Down
Loading
Loading