-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathDrawLib.lua
More file actions
350 lines (290 loc) · 9.45 KB
/
DrawLib.lua
File metadata and controls
350 lines (290 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
--- TODO
--
--- Test other Vector3 interpolation methods
--- Fix WorldLocToScreenPoint outside-of-screen bugging
--- Allow re-interpolation of curved path with fewer points based on distance to player
--- Make a proper Path object
DrawLib = {
name = "DrawLib",
version = {0,0,11},
tPaths = {},
tStyle = {
nLineWidth = 3,
crLineColor = ApolloColor.new(0/255, 160/255, 200/255):ToTable(),
bOutline = true,
},
-- Cached circle vectors
tCircle = setmetatable({}, {__index = function(self, nSides)
local tVectors = DrawLib:CalcCircleVectors(nSides)
self[nSides] = tVectors
return tVectors
end}),
}
function DrawLib:OnLoad()
self.xmlDoc = XmlDoc.CreateFromFile("DrawLib.xml")
self.wndOverlay = Apollo.LoadForm(self.xmlDoc, "Overlay", "InWorldHudStratum", self)
end
-- API
function DrawLib:UnitLine(unitSrc, unitDst, tStyle)
if unitSrc and unitSrc:IsValid() then
if unitDst and unitDst:IsValid() then
return self:Path({{unit = unitSrc}, {unit = unitDst}}, tStyle or self.tStyle)
else
Print("DrawLib: Invalid destination unit in DrawLib:UnitLine()")
end
else
Print("DrawLib: Invalid source unit in DrawLib:UnitLine()")
end
end
function DrawLib:UnitText(unit, text)
if unit and unit:IsValid() then
local wndMark = Apollo.LoadForm(self.xmlDoc, "unitMark", "FixedHudStratumHigh", self)
wndMark:FindChild("Text"):SetText(text)
return self:Path({{unit = unit, wndMark = wndMark}})
else
Print("DrawLib: Invalid unit in DrawLib:UnitText()")
end
end
function DrawLib:Text(pos, text, style)
local wndMark = Apollo.LoadForm(self.xmlDoc, "unitMark", "FixedHudStratumHigh", self)
local wndText = wndMark:FindChild("Text")
wndText:SetText(text)
if style and style.color then wndText:SetColor(style.color) end
if style and style.font then wndText:SetFont(style.font) end
return self:Path({{vPos = Vector3.New(pos), wndMark = wndMark}})
end
-- yep lets just go with yet another calling convention because I am a piece of shit
function DrawLib:Circle(args) -- target, nSides, nRadius, tStyle
if args.target == nil then return end
local nSides = args.nSides or 10
local nRadius = args.nRadius or 5
local tCircle = self.tCircle[nSides]
local tVertices = {}
for i=1,#tCircle do tVertices[i] = {vPos = tCircle[i]*nRadius} end
local tPath = self:Path(tVertices, args.tStyle)
if Unit.is(args.target) then tPath.unit = args.target end
if Vector3.Is(args.target) then tPath.vOffset = args.target end
if type(args.target) == "table" then tPath.vOffset = Vector3.New(args.target) end
return tPath
end
function DrawLib:UnitCircle(unit, fRadius, nSides, tStyle)
nSides = nSides or 10
fRadius = fRadius or 5
local tCircle = self.tCircle[nSides]
local tVertices = {}
for i=1,#tCircle do tVertices[i] = {vPos = tCircle[i]*fRadius} end
local tPath = self:Path(tVertices, tStyle)
tPath.unit = unit
return tPath
end
function DrawLib:Path(tVertices, tStyle)
if #self.tPaths == 0 then Apollo.RegisterEventHandler("NextFrame", "OnFrame", DrawLib) end
local tPath = {tVertices = tVertices, tStyle = setmetatable(tStyle or {}, {__index = self.tStyle})}
self.tPaths[#self.tPaths+1] = tPath
return tPath
end
function DrawLib:Destroy(tPath)
for i=#self.tPaths,1,-1 do
if self.tPaths[i] == tPath then
for _,tVertex in pairs(tPath.tVertices) do
if tVertex.wndMark then
tVertex.wndMark:Destroy()
tVertex.wndMark = nil
end
end
if tPath.tPixies then self:UpdatePixies(tPath.tPixies, {}) end
if tPath.tPixiesOutline then self:UpdatePixies(tPath.tPixiesOutline, {}) end
table.remove(self.tPaths,i)
end
end
if #self.tPaths == 0 then Apollo.RemoveEventHandler("NextFrame", self) end
end
-- Draw Handlers
function DrawLib:OnFrame()
for i=#self.tPaths,1,-1 do
self:DrawPath(self.tPaths[i])
end
end
function DrawLib:DrawPath(tPath)
local tScreenPoints = {}
local vOffset = tPath.vOffset
local fRotation
if tPath.unit then
if tPath.unit:IsValid() then
if vOffset then
vOffset = vOffset + Vector3.New(tPath.unit:GetPosition())
else
vOffset = Vector3.New(tPath.unit:GetPosition())
end
fRotation = math.pi + tPath.unit:GetHeading()
else
self:Destroy(tPath)
return
end
end
if self:UpdateVertices(tPath.tVertices, vOffset, fRotation) then
if tPath.tStyle.bOutline then
tPath.tPixiesOutline = tPath.tPixiesOutline or {}
self:UpdatePixies(tPath.tPixiesOutline, tPath.tVertices, tPath.tStyle, true)
elseif tPath.tPixiesOutline then
self:UpdatePixies(tPath.tPixiesOutline, {})
end
tPath.tPixies = tPath.tPixies or {}
self:UpdatePixies(tPath.tPixies, tPath.tVertices, tPath.tStyle)
else
self:Destroy(tPath)
end
end
function DrawLib:UpdateVertices(tVertices, vOffset, fRotation)
local fRotate = fRotation and self:Rotate(fRotation)
for i=1,#tVertices do
local vPoint
local tVertex = tVertices[i]
local unit = tVertex.unit
if unit then
if unit:IsValid() then
if tVertex.wndMark then tVertex.wndMark:SetUnit(unit) end
vPoint = Vector3.New(unit:GetPosition())
else
return false
end
else
vPoint = tVertex.vPos or Vector3.New(0,0,0)
if tVertex.vOffset then vPoint = vPoint + tVertex.vOffset end
if fRotate then vPoint = fRotate(vPoint) end
if vOffset then vPoint = vPoint + vOffset end
if tVertex.wndMark then tVertex.wndMark:SetWorldLocation(vPoint) end
end
tVertex.vPoint = vPoint
tVertex.tScreenPoint = GameLib.WorldLocToScreenPoint(vPoint)
end
return true
end
function DrawLib:UpdatePixies(tPixies, tVertices, tStyle, bOutline)
local overlay = self.wndOverlay
local length = math.max(#tPixies, #tVertices-1)
local sc = Apollo.GetDisplaySize() -- maybe move this out for performance
local scdiag = (sc.nWidth^2 + sc.nHeight^2)
for i=1,length do
if not tPixies[i] then tPixies[i] = {} end
local tPixie = tPixies[i]
local bDestroy = false
if tVertices[i] and tVertices[i+1] then
local pA = tVertices[i].tScreenPoint
local pB = tVertices[i+1].tScreenPoint
if pA.z < 0 and pB.z < 0 then bDestroy = true -- both points behind camera, nothing to draw
else
if pA.z < 0 and pB.z > 0 then pA,pB = pB,pA end -- swap
if pB.z < 0 and pA.z > 0 then
-- here, pB is the projection of a point behind the camera, where perspective projection breaks down;
-- to fix it, instead of drawing a line from pA to pB, we draw a line starting at pA going away from
-- the reflection of pB wrt to the center of the screen, and stopping somewhere off-screen.
--
-- ...don't ask me why it works. I'm no longer exactly sure myself.
local p = Vector3.New(sc.nWidth-pB.x, sc.nHeight-pB.y, 1)
-- to ensure we go off-screen, line length should be at least bigger than the length of screen diagonal,
-- but not too much, otherwise artifacts start to appear if pB is too far out
-- pB = pA + (pA-p)*100
pB = pA + (pA-p):NormalFast()*scdiag -- this might be overkill
end
local tConfig = tPixie.pixieConfig or {bLine = true, loc = {}}
tConfig.loc.nOffsets = {pA.x, pA.y, pB.x, pB.y}
if bOutline then
tConfig.fWidth = tStyle.nLineWidth + 2
tConfig.cr = "black"
else
tConfig.fWidth = tStyle.nLineWidth
tConfig.cr = tStyle.crLineColor
end
if tPixie.pixie then
overlay:UpdatePixie(tPixie.pixie, tPixie.pixieConfig)
else
tPixie.pixieConfig = tConfig
tPixie.pixie = overlay:AddPixie(tPixie.pixieConfig)
end
end
else
bDestroy = true
end
if bDestroy and tPixie.pixie then
overlay:DestroyPixie(tPixie.pixie)
tPixie.pixie = nil
end
end
end
-- Helpers
function DrawLib:CalcCircleVectors(nSides, fOffset)
local tVectors = {}
for i=0,nSides do
local angle = 2*i*math.pi/nSides + (fOffset or 0)
tVectors[i+1] = Vector3.New(-math.sin(angle), 0, -math.cos(angle))
end
return tVectors
end
function DrawLib:Rotate(fAngle)
local angleCos = math.cos(fAngle)
local angleSin = math.sin(fAngle)
return function(vPoint)
return Vector3.New(
angleCos*vPoint.x + angleSin*vPoint.z,
0,
-angleSin*vPoint.x + angleCos*vPoint.z
)
end
end
-- Leftover stuff
function DrawLib:CurvePath(tPath) -- native catmull-rom with 10 segments
local tCurvedPath = {}
for i=0,#tPath-2 do
local vA = (i>0) and tPath[i] or tPath[i+1]
local vB = tPath[i+1]
local vC = tPath[i+2]
local vD = (i<#tPath-2) and tPath[i+3] or tPath[i+2]
for j=1,10 do tCurvedPath[10*i+j] = Vector3.InterpolateCatmullRom(vA,vB,vC,vD,j/10) end
end
return tCurvedPath
end
function DrawLib:GetSqDistanceToSeg(vP,vA,vB)
local vC = vA
if vA ~= vB then
local vDir = vB - vA
local fSqLen = vDir:LengthSq()
local t = Vector3.Dot(vDir, vP - vA) / fSqLen
if t > 1 then vC = vB elseif t > 0 then vC = vA + vDir*t end
end
return (vP-vC):LengthSq()
end
function DrawLib:SimplifyPath(tPath, fTolerance) -- Ramer-Douglas-Peucker
local tSimplePath = {}
local tMarkers = {[1] = true, [#tPath] = true}
local index
local tStack = {#tPath, 1}
while #tStack > 0 do
local maxDist = 0
local first = tStack[#tStack]
tStack[#tStack] = nil
local last = tStack[#tStack]
tStack[#tStack] = nil
for i=first+1,last-1 do
local SqDist = self:GetSqDistanceToSeg(tPath[i],tPath[first],tPath[last])
if SqDist > maxDist then
maxDist = SqDist
index = i
end
end
if maxDist > fTolerance then
tMarkers[index] = true
tStack[#tStack+1] = last
tStack[#tStack+1] = index
tStack[#tStack+1] = index
tStack[#tStack+1] = first
end
end
for i=1,#tPath do
if tMarkers[i] then
tSimplePath[#tSimplePath+1] = tPath[i]
end
end
return tSimplePath
end
Apollo.RegisterAddon(DrawLib)