myspace / CommonLua /X /XMap.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
19.5 kB
local UIL = UIL
DefineClass.XMap = {
__parents = { "XControl" },
properties = {
{ id = "UseCustomTime", editor = "bool", default = true, help = "If true, map object animations are done in Lua time, which is read from hr.UIL_CustomTime." },
{ id = "map_size", name = "Map Size", editor = "point2d", default = point(1000, 1000) },
},
Clip = "self",
UseClipBox = false, -- Disable UIL level clipping of children. Renderer will clip.
MouseCursor = "UI/Cursors/Pda_Cursor.tga",
translation_modId = 0,
scale_modId = 1,
scroll_start_pt = false,
last_box = false,
last_current_scale = false,
last_scale = false,
MouseWheelStep = 300,
map_time_reference = false,
real_time_reference = false,
time_factor = 1000,
max_zoom = 5000,
rollover_padding = 15
}
function XMap:Init()
local mapModifier = {
id = "map-window",
type = const.intParamRect,
translateParam = self.translation_modId,
scaleParam = self.scale_modId,
interpolate_clip = false
}
-- Reset params
UIL.SetParam(self.translation_modId, 0, 0, 0)
UIL.SetParam(self.scale_modId, 1000, 1000, 0)
self:AddInterpolation(mapModifier)
self.map_time_reference = 0
self.real_time_reference = GetPreciseTicks()
end
function XMap:ScrollMap(dx, dy, time, int)
local transX, transY = UIL.GetParam(self.translation_modId, "end")
return self:SetMapScroll(transX + dx, transY + dy, time, int)
end
function XMap:SetMapScroll(transX, transY, time, int)
-- Clamp to map bounds.
local scale = UIL.GetParam(self.scale_modId, "end")
local win_box = self.box
transX = Clamp(transX, - self.map_size:x() * scale / 1000 + win_box:maxx() + 1, win_box:minx())
transY = Clamp(transY, - self.map_size:y() * scale / 1000 + win_box:maxy() + 1, win_box:miny())
UIL.SetParam(self.translation_modId, transX, transY, time or 100, int)
end
function XMap:CenterScrollOn(x, y, time)
local scaleX, scaleY = UIL.GetParam(self.scale_modId, "end")
x = MulDivRound(x, scaleX, 1000)
y = MulDivRound(y, scaleY, 1000)
local winSize = self.box:size()
self:SetMapScroll(winSize:x() / 2 - x, winSize:y() / 2 - y, time)
end
function XMap:ZoomMap(scale, time, origin_pos)
return self:SetMapZoom(UIL.GetParam(self.scale_modId, "end") + scale, time, origin_pos)
end
function XMap:GetScaledMaxZoom()
return MulDivRound(self.max_zoom, self.scale:x(), 1000)
end
function XMap:SetMapZoom(scale, time, origin_pos)
local current_scale = UIL.GetParam(self.scale_modId)
local min_scale = Max(1000 * self.box:sizex() / self.map_size:x(), 1000 * self.box:sizey() / self.map_size:y())
scale = Clamp(scale, min_scale, self:GetScaledMaxZoom())
time = time or 100
UIL.SetParam(self.scale_modId, scale, scale, time)
if origin_pos then
local transX, transY = UIL.GetParam(self.translation_modId)
local dx = origin_pos:x() - MulDivRound(origin_pos:x() - transX, scale, current_scale)
local dy = origin_pos:y() - MulDivRound(origin_pos:y() - transY, scale, current_scale)
self:SetMapScroll(dx, dy, time)
end
self.last_scale = current_scale
self.current_scale = scale
for _, win in ipairs(self) do
if win.UpdateZoom then
win:UpdateZoom(current_scale, scale, time)
end
end
end
function XMap:MapToScreenPt(pos, time)
local scaleX, scaleY = UIL.GetParam(self.scale_modId, time)
local transX, transY = UIL.GetParam(self.translation_modId, time)
return point(
MulDivRound(pos:x(), scaleX, 1000) + transX,
MulDivRound(pos:y(), scaleY, 1000) + transY
)
end
function XMap:MapToScreenBox(b, time)
local scaleX, scaleY = UIL.GetParam(self.scale_modId, time)
local transX, transY = UIL.GetParam(self.translation_modId, time)
return box(
MulDivRound(b:minx(), scaleX, 1000) + transX,
MulDivRound(b:miny(), scaleY, 1000) + transY,
MulDivRound(b:maxx(), scaleX, 1000) + transX,
MulDivRound(b:maxy(), scaleY, 1000) + transY
)
end
function XMap:ScreenToMapPt(pos, time)
local scaleX, scaleY = UIL.GetParam(self.scale_modId, time)
local transX, transY = UIL.GetParam(self.translation_modId, time)
return point(
MulDivRound(pos:x() - transX, 1000, scaleX),
MulDivRound(pos:y() - transY, 1000, scaleY)
)
end
function XMap:ScreenToMapBox(b, time)
local scaleX, scaleY = UIL.GetParam(self.scale_modId, time)
local transX, transY = UIL.GetParam(self.translation_modId, time)
return box(
MulDivRound(b:minx() - transX, 1000, scaleX),
MulDivRound(b:miny() - transY, 1000, scaleY),
MulDivRound(b:maxx() - transX, 1000, scaleX),
MulDivRound(b:maxy() - transY, 1000, scaleY)
)
end
-- Don't move children as their positions are relative to the map.
function XMap:SetBox(x, y, width, height, move_children, ...)
return XWindow.SetBox(self, x, y, width, height, "dont-move", ...)
end
function XMap:Layout(x, y, width, height)
-- do not use a layout method to position the children, this is already done
end
function XMap:UpdateLayout()
for _, win in ipairs(self) do
if IsKindOf(win, "XMapWindow") then
assert(win.Dock == "ignore") -- XMapWindow children should not participate in the "classical" layout, they are positioned here
win:SetBox(win:GetMapSpaceBox())
end
end
XControl.UpdateLayout(self)
end
function XMap:OnLayoutComplete()
if self.last_box ~= self.box then
-- Force a reclamp to the map area.
self:ScrollMap(0, 0, 0)
self:ZoomMap(0, 0)
self.last_box = self.box
else
-- Children's UpdateZoom needs to be ran after layout.
for _, win in ipairs(self) do
if win.UpdateZoom then
win:UpdateZoom(self.last_scale, self.current_scale, 0)
end
end
end
end
function XMap:GetMouseTarget(pt)
return XWindow.GetMouseTarget(self, self:ScreenToMapPt(pt))
end
function XMap:OnMouseButtonDown(pt, button)
if button == "L" or button == "M" then
self.scroll_start_pt = pt
UIL.SetParam(self.translation_modId, UIL.GetParam(self.translation_modId))
self:ScrollStart()
end
end
function XMap:ScrollStart()
end
function XMap:ScrollStop()
self.scroll_start_pt = false
end
function XMap:OnMouseButtonUp(pt, button)
if button == "L" or button == "M" then
-- Calculations here are based on what feels good, and are not scientific :P
local prevPos = self:ScreenToMapPt(self.scroll_start_pt or pt)
local currentPos = self:ScreenToMapPt(pt)
local diff = (currentPos - prevPos)
local diffClamped = Min(diff:Len(), 500)
local inertiaPower = Lerp(500, 1, diffClamped, 500) -- boost inertia based on difference
inertiaPower = inertiaPower / 100
diff = diff * inertiaPower
self:ScrollStop()
self:ScrollMap(diff:x(), diff:y(), 500, "cubic out") -- interpolate based on distance
end
end
function XMap:OnMousePos(pt)
if self.scroll_start_pt then
self:ScrollMap(pt:x() - self.scroll_start_pt:x(), pt:y() - self.scroll_start_pt:y(), 25)
self.scroll_start_pt = pt
end
end
function XMap:OnMouseWheelForward(pos)
self:ScrollStop()
self:ZoomMap(self.MouseWheelStep, 100, pos)
return "break"
end
function XMap:OnMouseWheelBack(pos)
self:ScrollStop()
self:ZoomMap(-self.MouseWheelStep, 100, pos)
return "break"
end
function XMap:OnMouseLeft()
self:ScrollStop()
end
function XMap:OnCaptureLost()
self:ScrollStop()
end
function XMap:SetTimeFactor(factor)
local mapTime = self:GetMapTime()
self.map_time_reference = mapTime
self.real_time_reference = GetPreciseTicks()
local oldFactor = factor
self.time_factor = factor
for _, win in ipairs(self) do
if win.UpdateTimeFactor then
win:UpdateTimeFactor(oldFactor, factor, mapTime)
end
end
end
function XMap:GetMapTime()
local timeDifference = GetPreciseTicks() - self.real_time_reference
return self.map_time_reference + MulDivRound(timeDifference, self.time_factor, 1000)
end
----- XMapWindow
DefineClass.XMapWindow = {
__parents = { "XWindow", "XMapRolloverable" },
Dock = "ignore",
UseClipBox = false,
ScaleWithMap = true,
HAlign = "center",
VAlign = "center",
-- Position in map coordinates
PosX = 0,
PosY = 0,
map = false
}
function XMapWindow:SetWidth(width)
self.MinWidth = width
self.MaxWidth = width
end
function XMapWindow:SetHeight(height)
self.MinHeight = height
self.MaxHeight = height
end
function XMapWindow:GetMapSpaceBox()
local minX, minY = ScaleXY(self.scale, self.MinWidth, self.MinHeight)
local maxX, maxY = ScaleXY(self.scale, self.MaxWidth, self.MaxHeight)
local width = Clamp(self.measure_width, minX, maxX)
local height = Clamp(self.measure_height, minY, maxY)
local x, y = self.PosX, self.PosY
local HAlign, VAlign = self.HAlign, self.VAlign
if HAlign == "center" then
x = x - width / 2
elseif HAlign == "right" then
x = x + width
end
if VAlign == "center" then
y = y - height / 2
elseif VAlign == "bottom" then
y = y + height
end
return x, y, width, height
end
function XMapWindow:SetParent(...)
XWindow.SetParent(self, ...)
self.map = GetParentOfKind(self, "XMap")
end
function XMapWindow:UpdateMeasure(max_width, max_height)
if self.map then
max_width, max_height = self.map.map_size:xy()
end
return XWindow.UpdateMeasure(self, max_width, max_height)
end
function XMapWindow:PointInWindow(pt)
local f = pt[self.Shape] or pt.InBox
local box = self:GetInterpolatedBox(false, self.interaction_box)
return f(pt, box)
end
local no_scale = point(1000, 1000)
function XMapWindow:SetOutsideScale(scale)
-- scale is already taken into account in XMap
return XWindow.SetOutsideScale(self, self.ScaleWithMap and no_scale or scale)
end
function XMapWindow:UpdateTimeFactor(oldFactor, newFactor, currentMapTime)
for i, w in ipairs(self) do
if w.UpdateTimeFactor then
w:UpdateTimeFactor(oldFactor, newFactor, currentMapTime)
end
end
end
function XMapWindow:UpdateZoom(prevZoom, newZoom, time)
if self.ScaleWithMap then
self:RemoveModifier("reverse-zoom")
return
end
self:AddInterpolation({
id = "reverse-zoom",
type = const.intRect,
interpolate_clip = false,
OnLayoutComplete = function(modifier, window)
modifier.originalRect = sizebox(self.PosX, self.PosY, newZoom, newZoom)
modifier.targetRect = sizebox(self.PosX, self.PosY, 1000, 1000)
end,
duration = 0
})
end
-- Should be inherited by child windows of XMapWindows
DefineClass.XMapRolloverable = {
__parents = { "XWindow", "XRollover" }
}
function XMapRolloverable:ResolveRolloverAnchor(context, pos)
local b = #(self.RolloverAnchorId or "") > 0 and XRollover.ResolveRolloverAnchor(self, context, pos) or self:GetInterpolatedBox()
local map = GetParentOfKind(self, "XMap")
return map and map:MapToScreenBox(b, "end") or b
end
function XMapRolloverable:SetupMapSafeArea(wnd)
local map = GetParentOfKind(self, "XMap")
wnd.GetSafeAreaBox = function()
local x, y, mx, my = map.box:xyxy()
local rolloverPadX, rolloverPadY = ScaleXY(map.scale, map.rollover_padding, map.rollover_padding)
return x + rolloverPadX, y + rolloverPadY, mx - rolloverPadX * 2, my - rolloverPadY * 2
end
wnd.GetAnchor = function(s)
return self:ResolveRolloverAnchor(wnd.context)
end
end
function XMapRolloverable:CreateRolloverWindow(gamepad, context, pos)
context = SubContext(self:GetContext(), context)
context.control = self
context.anchor = self:ResolveRolloverAnchor(context, pos)
context.gamepad = gamepad
local win = XTemplateSpawn(self:GetRolloverTemplate(), nil, context)
if not win then return false end
self:SetupMapSafeArea(win)
win:Open()
local map = GetParentOfKind(self.parent, "XMap")
if map then
DelayedCall(0, function()
map:InvalidateLayout()
end)
end
return win
end
----- XMapObject
DefineClass.XMapObject = {
__parents = { "XMapWindow" },
HandleMouse = true,
currentInterp = false,
currentResize = false,
}
function XMapObject:GetPos()
return point(self.PosX, self.PosY)
end
function XMapObject:GetVisualPos()
local uiBox = self.layout_update and sizebox(self:GetMapSpaceBox()) or self.box
local b = self:GetInterpolatedBox("move", uiBox)
local x, y = b:minxyz()
local width, height = uiBox:sizexyz()
local HAlign, VAlign = self.HAlign, self.HAlign
if HAlign == "center" then
x = x + width / 2
elseif HAlign == "right" then
x = x - width
end
if VAlign == "center" then
y = y + height / 2
elseif VAlign == "bottom" then
y = y - height
end
return point(x, y)
end
function XMapObject:GetVisualSize()
local uiBox = self.layout_update and sizebox(self:GetMapSpaceBox()) or self.box
local b = self:GetInterpolatedBox("resize", uiBox)
return b:size()
end
function XMapObject:UpdateTimeFactor(...)
if self.currentInterp then
local interp = self.currentInterp
local x, y, time = self:GetContinueInterpolationParams(interp.startX, interp.startY, interp.endX, interp.endY, interp.time, self:GetVisualPos())
if x then self:SetPos(x, y, time) end
end
if self.currentResize then
local interp = self.currentResize
local x, y, time = self:GetContinueInterpolationParams(interp.startX, interp.startY, interp.endX, interp.endY, interp.time, self:GetVisualSize())
if x then self:SetSize(x, y, time, interp.hOrigin, interp.vOrigin) end
end
XMapWindow.UpdateTimeFactor(self, ...)
end
function GetContinueInterpolationParams(startX, startY, endX, endY, totalTime, currentXY)
-- Inverse lerp to find where along the path the object is
local start = point(startX, startY)
local diff = point(endX, endY) - start
local passed = currentXY - start
local passedDDiff = Dot(passed, diff)
local percentPassed = passedDDiff ~= 0 and MulDivRound(passedDDiff, 1000, Dot(diff, diff)) or 0
-- Find time left (in real time)
local timeLeft = totalTime - MulDivRound(totalTime, percentPassed, 1000)
if timeLeft <= 0 then
return false
end
return endX, endY, timeLeft
end
function XMapObject:GetContinueInterpolationParams(...)
return GetContinueInterpolationParams(...)
end
function XMapObject:SetPos(posX, posY, time)
if not time or time == 0 then
if self:RemoveModifier("move") then
Msg(self)
end
self.currentInterp = false
if self.PosX == posX and self.PosY == posY then return end
self.PosX = posX
self.PosY = posY
self:InvalidateLayout()
return
end
if self:FindModifier("move") then
-- Find where we've interpolated to and apply that as the current pos.
local visualPos = self:GetVisualPos()
self.PosX, self.PosY = visualPos:xy()
end
self.currentInterp = { startX = self.PosX, startY = self.PosY, endX = posX, endY = posY, time = time }
local map = self.map
if map then
if map.time_factor == 0 then
time = 0
else
time = MulDivRound(time, 1000, map.time_factor)
end
end
local diffX = self.PosX - posX
local diffY = self.PosY - posY
self.PosX = posX
self.PosY = posY
self:InvalidateLayout()
self:AddInterpolation({
id = "move",
type = const.intRect,
interpolate_clip = false,
OnLayoutComplete = XMapObjectInterpolationOnLayoutComplete,
originalRect = sizebox(0, 0, 1000, 1000),
targetRect = sizebox(diffX, diffY, 1000, 1000),
duration = time,
autoremove = time ~= 0,
on_complete = function()
if time == 0 then return end -- Frozen time
self.currentInterp = false
Msg(self)
end,
flags = map.UseCustomTime and bor(const.intfInverse, const.intfUILCustomTime) or const.intfInverse,
no_invalidate_on_remove = true
}, 1)
end
local function lGetResizeOrigins(hOrigin, vOrigin, width, height, startWidth, startHeight)
local posX, posY = 0, 0
if hOrigin == "right" then
posX = startWidth - width
end
if vOrigin == "bottom" then
posY = startHeight - height
end
return posX, posY
end
function XMapObject:SetSize(width, height, time, hOrigin, vOrigin)
if not time or time == 0 then
self:RemoveModifier("resize")
self.currentResize = false
local posX, posY = lGetResizeOrigins(hOrigin, vOrigin, width, height, self.MaxWidth, self.MaxHeight)
self.PosX = self.PosX + posX
self.PosY = self.PosY + posY
self:SetWidth(width)
self:SetHeight(height)
self:InvalidateLayout()
self:InvalidateMeasure()
return
end
if self:FindModifier("resize") then
local visualSize = self:GetVisualSize()
local vwidth, vheight = visualSize:xy()
-- Calculate position offset
local current = self.currentResize
local posX, posY = lGetResizeOrigins(current.hOrigin, current.vOrigin, vwidth, vheight, current.startX, current.startY)
self.PosX = self.PosX + posX
self.PosY = self.PosY + posY
self:SetWidth(vwidth)
self:SetHeight(vheight)
self:RemoveModifier("resize")
self:InvalidateMeasure()
self:InvalidateLayout()
end
self.currentResize = {
startX = self.MaxWidth,
startY = self.MaxHeight,
endX = width,
endY = height,
time = time,
hOrigin = hOrigin,
vOrigin = vOrigin
}
local map = self.map
if map then
if map.time_factor == 0 then
time = 0
else
time = MulDivRound(time, 1000, map.time_factor)
end
end
if time == 0 then
return
end
--[[ if time == 0 then
local visSize = self:GetVisualSize()
width = visSize:x()
height = visSize:y()
end]]
local posX, posY = lGetResizeOrigins(hOrigin, vOrigin, width, height, self.MaxWidth, self.MaxHeight)
self:AddInterpolation{
id = "resize",
type = const.intRect,
interpolate_clip = false,
OnLayoutComplete = XMapObjectInterpolationOnLayoutComplete,
originalRect = sizebox(0, 0, self.MaxWidth, self.MaxHeight),
targetRect = sizebox(posX, posY, width, height),
duration = time,
autoremove = time ~= 0,
on_complete = function()
if time == 0 then return end -- Frozen time
self.currentResize = false
self.PosX = self.PosX + posX
self.PosY = self.PosY + posY
self:SetWidth(width)
self:SetHeight(height)
self:InvalidateMeasure()
self:InvalidateLayout()
end,
flags = map.UseCustomTime and const.intfUILCustomTime,
}
self:InvalidateLayout()
self:InvalidateMeasure()
end
function XMapObjectInterpolationOnLayoutComplete(modifier, window)
local ogRect = modifier.unoffsetOgRect or modifier.originalRect
local tarRect = modifier.unoffsetTarRect or modifier.targetRect
if not modifier.unoffsetOgRect then
modifier.unoffsetOgRect = ogRect
modifier.unoffsetTarRect = tarRect
end
modifier.originalRect = Offset(ogRect, window.box:min())
modifier.targetRect = Offset(tarRect, window.box:min())
end
----- Test
if FirstLoad then
winTest = false
end
function XMap:DrawContent()
-- Draw debug checkerboard
local colorOne = RGB(255, 255, 255)
local colorTwo = RGB(0, 0, 0)
for y = 0, self.map_size:y(), 100 do
for x = 0, self.map_size:x(), 100 do
local color = (x / 100 + y / 100) % 2 == 0 and colorTwo or colorOne
UIL.DrawSolidRect(sizebox(x, y, 100, 100), color)
end
end
end
function TestXMap()
if winTest then
winTest:Close()
winTest = false
end
winTest = XTemplateSpawn("XWindow")
winTest:SetMinWidth(1200)
winTest:SetMinHeight(800)
winTest:SetMaxWidth(1200)
winTest:SetMaxHeight(800)
winTest:SetHAlign("center")
winTest:SetVAlign("center")
winTest:SetId("idTest")
local map = XTemplateSpawn("XMap", winTest)
rawset(map, "test_obj", false)
map.UseCustomTime = false
map.OnMouseButtonUp = function(self, pt, button)
if button == "R" then
local map_pos = self:ScreenToMapPt(pt)
if self.test_obj then
self.test_obj:SetPos(map_pos:x(), map_pos:y(), 300)
else
self.test_obj = AddTestObjectToMap(map_pos:xy())
end
end
return XMap.OnMouseButtonUp(self, pt, button)
end
winTest:Open()
end
function AddTestObjectToMap(posX, posY, map)
map = map or winTest and winTest[1]
if not map then return end
local obj = XTemplateSpawn("XMapObject", map)
obj:SetBackground(RGB(44, 88, 151))
obj.PosX = posX
obj.PosY = posY
obj:SetWidth(30)
obj:SetHeight(60)
obj:Open()
return obj
end
--]]