Skip to content

Commit 3a309f6

Browse files
committed
Fixed plotting and added snapping guides
1 parent e209394 commit 3a309f6

File tree

8 files changed

+523
-30
lines changed

8 files changed

+523
-30
lines changed

src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt

Lines changed: 169 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,195 @@ package com.lambda.gui.components
2020
import com.lambda.core.Loadable
2121
import com.lambda.event.events.GuiEvent
2222
import com.lambda.event.listener.SafeListener.Companion.listen
23+
import com.lambda.gui.dsl.ImGuiBuilder
2324
import com.lambda.gui.dsl.ImGuiBuilder.buildLayout
25+
import com.lambda.gui.snap.Guide
26+
import com.lambda.gui.snap.RectF
27+
import com.lambda.gui.snap.SnapManager
2428
import com.lambda.module.HudModule
2529
import com.lambda.module.ModuleRegistry
30+
import com.lambda.module.modules.client.GuiSettings
31+
import com.lambda.module.modules.client.ClickGui
32+
import imgui.ImGui
33+
import imgui.ImDrawList
34+
import imgui.flag.ImDrawListFlags
2635
import imgui.flag.ImGuiWindowFlags
36+
import kotlin.math.PI
2737

2838
object HudGuiLayout : Loadable {
2939
const val DEFAULT_HUD_FLAGS =
3040
ImGuiWindowFlags.NoDecoration or
3141
ImGuiWindowFlags.NoBackground or
3242
ImGuiWindowFlags.AlwaysAutoResize or
3343
ImGuiWindowFlags.NoDocking
44+
private var activeDragHudName: String? = null
45+
private var mouseWasDown = false
46+
private var dragOffsetX = 0f
47+
private var dragOffsetY = 0f
48+
private val lastBounds = mutableMapOf<String, RectF>()
49+
private val pendingPositions = mutableMapOf<String, Pair<Float, Float>>()
50+
private val snapOverlays = mutableMapOf<String, SnapVisual>()
51+
52+
private data class SnapVisual(
53+
val snapX: Float?,
54+
val snapY: Float?,
55+
val kindX: Guide.Kind?,
56+
val kindY: Guide.Kind?
57+
)
58+
59+
// Precomputed Float PI values to avoid repeated conversions
60+
private const val PI_F = PI.toFloat()
61+
private const val HALF_PI_F = (0.5f * PI).toFloat()
62+
private const val THREE_HALVES_PI_F = (1.5f * PI).toFloat()
63+
private const val TWO_PI_F = (2f * PI).toFloat()
3464

3565
init {
3666
listen<GuiEvent.NewFrame> {
3767
buildLayout {
38-
ModuleRegistry.modules
68+
val vp = ImGui.getMainViewport()
69+
SnapManager.beginFrame(vp.sizeX, vp.sizeY, io.fontGlobalScale)
70+
71+
val mouseDown = io.mouseDown[0]
72+
val mousePressedThisFrame = mouseDown && !mouseWasDown
73+
val mouseReleasedThisFrame = !mouseDown && mouseWasDown
74+
mouseWasDown = mouseDown
75+
if (mouseReleasedThisFrame) {
76+
activeDragHudName = null
77+
}
78+
79+
pendingPositions.clear()
80+
snapOverlays.clear()
81+
82+
val huds = ModuleRegistry.modules
3983
.filterIsInstance<HudModule>()
4084
.filter { it.isEnabled }
41-
.forEach { hud ->
42-
window("##${hud.name}", flags = DEFAULT_HUD_FLAGS) {
43-
with(hud) { buildLayout() }
85+
86+
if (ClickGui.isEnabled && activeDragHudName == null && mousePressedThisFrame) {
87+
tryBeginDrag(huds)
88+
}
89+
90+
if (ClickGui.isEnabled && activeDragHudName != null && mouseDown) {
91+
updateDragAndSnapping()
92+
}
93+
94+
huds.forEach { hud ->
95+
val override = pendingPositions[hud.name]
96+
if (override != null) {
97+
ImGui.setNextWindowPos(override.first, override.second)
98+
}
99+
window("##${hud.name}", flags = DEFAULT_HUD_FLAGS) {
100+
val vis = snapOverlays[hud.name]
101+
if (vis != null) {
102+
SnapManager.drawSnapLines(
103+
foregroundDrawList,
104+
vis.snapX, vis.kindX,
105+
vis.snapY, vis.kindY
106+
)
44107
}
108+
with(hud) { buildLayout() }
109+
// Rounded-corner only outline; pull parameters from settings
110+
if (ClickGui.isEnabled) {
111+
drawHudOutline(
112+
draw = foregroundDrawList,
113+
x = windowPos.x,
114+
y = windowPos.y,
115+
w = windowSize.x,
116+
h = windowSize.y
117+
)
118+
}
119+
val p = windowPos
120+
val s = windowSize
121+
val rect = RectF(p.x, p.y, s.x, s.y)
122+
SnapManager.registerElement(hud.name, rect)
123+
lastBounds[hud.name] = rect
45124
}
125+
}
46126
}
47127
}
48128
}
129+
130+
private fun ImGuiBuilder.tryBeginDrag(huds: List<HudModule>) {
131+
val mx = io.mousePos.x
132+
val my = io.mousePos.y
133+
huds.forEach { hud ->
134+
val r = lastBounds[hud.name] ?: return@forEach
135+
val inside = mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h
136+
if (inside) {
137+
activeDragHudName = hud.name
138+
dragOffsetX = mx - r.x
139+
dragOffsetY = my - r.y
140+
return
141+
}
142+
}
143+
}
144+
145+
private fun ImGuiBuilder.updateDragAndSnapping() {
146+
val id = activeDragHudName ?: return
147+
val last = lastBounds[id] ?: return
148+
val mx = io.mousePos.x
149+
val my = io.mousePos.y
150+
val targetX = mx - dragOffsetX
151+
val targetY = my - dragOffsetY
152+
val proposed = RectF(targetX, targetY, last.w, last.h)
153+
val snap = SnapManager.computeSnap(proposed, id)
154+
val finalX = targetX + snap.dx
155+
val finalY = targetY + snap.dy
156+
pendingPositions[id] = finalX to finalY
157+
snapOverlays[id] = SnapVisual(snap.snapX, snap.snapY, snap.kindX, snap.kindY)
158+
}
159+
160+
private fun ImGuiBuilder.drawHudOutline(draw: ImDrawList, x: Float, y: Float, w: Float, h: Float) {
161+
val baseRadius = GuiSettings.hudOutlineCornerRadius
162+
val rounding = if (baseRadius > 0f) baseRadius else style.windowRounding
163+
val inflate = GuiSettings.hudOutlineCornerInflate
164+
// Soft halo corners (gray, slightly smaller)
165+
drawCornerArcs(
166+
draw,
167+
x, y, w, h,
168+
(rounding + inflate).coerceAtLeast(0f),
169+
GuiSettings.hudOutlineHaloColor.rgb,
170+
GuiSettings.hudOutlineHaloThickness
171+
)
172+
// Crisp inner corner arcs
173+
drawCornerArcs(
174+
draw,
175+
x, y, w, h,
176+
rounding.coerceAtLeast(0f),
177+
GuiSettings.hudOutlineBorderColor.rgb,
178+
GuiSettings.hudOutlineBorderThickness
179+
)
180+
}
181+
182+
private fun drawCornerArcs(
183+
draw: ImDrawList,
184+
x: Float, y: Float, w: Float, h: Float,
185+
radius: Float,
186+
color: Int,
187+
thickness: Float
188+
) {
189+
if (radius <= 0f || thickness <= 0f) return
190+
val tlCx = x + radius
191+
val tlCy = y + radius
192+
val trCx = x + w - radius
193+
val trCy = y + radius
194+
val brCx = x + w - radius
195+
val brCy = y + h - radius
196+
val blCx = x + radius
197+
val blCy = y + h - radius
198+
199+
fun strokeArc(cx: Float, cy: Float, start: Float, end: Float) {
200+
draw.pathClear()
201+
draw.pathArcTo(cx, cy, radius, start, end, 0)
202+
draw.pathStroke(color, ImDrawListFlags.None, thickness)
203+
}
204+
205+
// TL: pi -> 1.5pi
206+
strokeArc(tlCx, tlCy, PI_F, THREE_HALVES_PI_F)
207+
// TR: 1.5pi -> 2pi
208+
strokeArc(trCx, trCy, THREE_HALVES_PI_F, TWO_PI_F)
209+
// BR: 0 -> 0.5pi
210+
strokeArc(brCx, brCy, 0f, HALF_PI_F)
211+
// BL: 0.5pi -> pi
212+
strokeArc(blCx, blCy, HALF_PI_F, PI_F)
213+
}
49214
}

src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,7 +1272,7 @@ object ImGuiBuilder {
12721272
* @param scaleMin Minimum scale value
12731273
* @param scaleMax Maximum scale value
12741274
* @param graphSize Size of the graph
1275-
* @param stride Stride between values
1275+
* @param stride Sample decimation step (>= 1). Use 0/1 for contiguous data.
12761276
*/
12771277
@ImGuiDsl
12781278
fun plotLines(
@@ -1283,8 +1283,17 @@ object ImGuiBuilder {
12831283
scaleMin: Float = Float.MAX_VALUE,
12841284
scaleMax: Float = Float.MAX_VALUE,
12851285
graphSize: ImVec2 = ImVec2(),
1286-
stride: Int = 1,
1287-
) = ImGui.plotLines(label, values, valuesOffset, overlayText, scaleMin, scaleMax, graphSize, stride)
1286+
stride: Int = 0,
1287+
) {
1288+
val (src, sMin, sMax) = preparePlotSeries(values, stride, scaleMin, scaleMax)
1289+
val count = src.size
1290+
val offset = if (count == 0) 0 else valuesOffset.coerceIn(0, count - 1)
1291+
if (count < 2) {
1292+
plotLines(label, floatArrayOf(), 0, 0, overlayText, sMin, sMax, graphSize.x, graphSize.y)
1293+
return
1294+
}
1295+
plotLines(label, src, count, offset, overlayText, sMin, sMax, graphSize.x, graphSize.y)
1296+
}
12881297

12891298
/**
12901299
* Creates a plot of histogram values.
@@ -1296,7 +1305,7 @@ object ImGuiBuilder {
12961305
* @param scaleMin Minimum scale value
12971306
* @param scaleMax Maximum scale value
12981307
* @param graphSize Size of the graph
1299-
* @param stride Stride between values
1308+
* @param stride Sample decimation step (>= 1). Use 0/1 for contiguous data.
13001309
*/
13011310
@ImGuiDsl
13021311
fun plotHistogram(
@@ -1307,8 +1316,61 @@ object ImGuiBuilder {
13071316
scaleMin: Float = Float.MAX_VALUE,
13081317
scaleMax: Float = Float.MAX_VALUE,
13091318
graphSize: ImVec2 = ImVec2(),
1310-
stride: Int = 1,
1311-
) = ImGui.plotHistogram(label, values, valuesOffset, overlayText, scaleMin, scaleMax, graphSize, stride)
1319+
stride: Int = 0,
1320+
) {
1321+
val (src, sMin, sMax) = preparePlotSeries(values, stride, scaleMin, scaleMax)
1322+
val count = src.size
1323+
val offset = if (count == 0) 0 else valuesOffset.coerceIn(0, count - 1)
1324+
if (count < 1) {
1325+
plotHistogram(label, floatArrayOf(), 0, 0, overlayText, sMin, sMax, graphSize.x, graphSize.y)
1326+
return
1327+
}
1328+
plotHistogram(label, src, count, offset, overlayText, sMin, sMax, graphSize.x, graphSize.y)
1329+
}
1330+
1331+
private fun preparePlotSeries(
1332+
values: FloatArray,
1333+
stride: Int,
1334+
scaleMin: Float,
1335+
scaleMax: Float
1336+
): Triple<FloatArray, Float, Float> {
1337+
val contiguous = (stride <= 1)
1338+
val src = if (contiguous) {
1339+
values
1340+
} else {
1341+
val outSize = (values.size + stride - 1) / stride
1342+
val out = FloatArray(outSize)
1343+
var i = 0
1344+
var j = 0
1345+
while (i < values.size) {
1346+
out[j++] = values[i]
1347+
i += stride
1348+
}
1349+
out
1350+
}
1351+
var sMin = scaleMin
1352+
var sMax = scaleMax
1353+
if (sMin == Float.MAX_VALUE && sMax == Float.MAX_VALUE) {
1354+
var minV = Float.POSITIVE_INFINITY
1355+
var maxV = Float.NEGATIVE_INFINITY
1356+
for (v in src) if (v.isFinite()) {
1357+
if (v < minV) minV = v
1358+
if (v > maxV) maxV = v
1359+
}
1360+
if (!minV.isFinite() || !maxV.isFinite()) {
1361+
minV = 0f; maxV = 1f
1362+
}
1363+
if (minV == maxV) {
1364+
val base = if (minV == 0f) 1f else kotlin.math.abs(minV)
1365+
val pad = base * 0.01f
1366+
minV -= pad
1367+
maxV += pad
1368+
}
1369+
sMin = minV
1370+
sMax = maxV
1371+
}
1372+
return Triple(src, sMin, sMax)
1373+
}
13121374

13131375
/**
13141376
* Creates a main menu bar.
@@ -1485,7 +1547,7 @@ object ImGuiBuilder {
14851547
*
14861548
* @param title Title of the modal
14871549
* @param value Boolean reference to control visibility
1488-
* @param flags Additional window flags
1550+
* @param windowFlags Additional window flags
14891551
* @param block Content of the modal
14901552
*
14911553
* @see ImGuiPopupFlags
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.gui.snap
19+
20+
data class Guide(
21+
val orientation: Orientation,
22+
val pos: Float,
23+
val strength: Int,
24+
val kind: Kind
25+
) {
26+
enum class Orientation { Vertical, Horizontal }
27+
enum class Kind { ElementEdge, ElementCenter, ScreenCenter, Grid }
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.gui.snap
19+
20+
data class RectF(val x: Float, val y: Float, val w: Float, val h: Float) {
21+
val left get() = x
22+
val right get() = x + w
23+
val top get() = y
24+
val bottom get() = y + h
25+
val cx get() = x + w * 0.5f
26+
val cy get() = y + h * 0.5f
27+
}

0 commit comments

Comments
 (0)