evolve
Store/Shared libraries

lane_geom

A lane as a polyline: the point on it nearest to a creep, the heading there, and a walk of N units along it.

hunnyc0 scripts use it
SupportSource is readable
Use it in a script
pack.json
"requires": { "lane_geom": "^1.0" }
your lua
local lane_geom = require("lib/lane_geom")

Players install nothing: the script brings the library, any version inside the range. Edit scripts/lib/lane_geom/ in a dev session to try a change before you publish.

API
lane_geom.project(wps: table[], pos: table): number, number#

arc length along the lane of the point nearest to pos, and its distance from the lane.

Parameters
wps table[] waypoints in index order
pos table a point with GetX/GetY or x/y
Returns
number
number
lane_geom.heading(wps: table[], pos: table, dir: integer, span?: number): number|nil, number|nil#

unit heading of the lane at pos, going the way dir says (+1 = rising waypoint index,

Parameters
wps table[]
pos table
dir integer
span number default 150
Returns
number|nil
number|nil

-1 = falling). taken over span units either side of the point, so a corner turns the arrow over span rather than flipping it at the waypoint. nil for a lane of one point.

lane_geom.walk(wps: table[], pos: table, dist: number, dir: integer): number, number#

where a creep that stands at pos is after walking dist along the lane: projected on

Parameters
wps table[]
pos table
dist number
dir integer +1 rising waypoint index, -1 falling
Returns
number
number

the lane first, then along it, corners included. stops at the lane's end.

lane_geom.walk_point(wps: table[], pos: table, dist: number, dir: integer): table#

walk as a Vector (z of the start point kept), for callers that keep Vectors.

Parameters
wps table[]
pos table
dist number
dir integer
Returns
table
lane_geom.angle(wps: table[], pos: table, dir: integer, span?: number): number|nil#

heading as an angle in world axes, radians (atan2(dy, dx)); nil as heading.

Parameters
wps table[]
pos table
dir integer
span number default 150
Returns
number|nil
Overview

lib/lane_geom

A lane as a polyline: the point on it nearest to a creep, the heading there, and a walk of N units along it. The four uTEA copies of walk_along_waypoints (awareness, naga, natureprophet, clockwerk) took the nearest waypoint and went straight to the next one, which cuts a corner and points the wrong way near it; awareness lane_dir did the same for the minimap arrow (#191). On the top and bot lanes of LANE_WAYPOINTS that was a heading off by more than 20 degrees on 11-14% of the lane, 61 at worst, and a fog walk of 1500 u that ended up to 250 u off the lane.

local LG = require("lib/lane_geom")

local dx, dy = LG.heading(wps, pos, -1)        -- unit heading, falling waypoint index
local x, y   = LG.walk(wps, pos, 1500, -1)     -- after 1500 u on the lane
local v      = LG.walk_point(wps, pos, 1500, -1)   -- the same as a Vector, z of pos kept
local s, off = LG.project(wps, pos)            -- arc length from waypoint 1, distance off the lane

wps is a list of points (Vector or {x=, y=}) in lane order. dir is +1 for rising index, -1 for falling. heading is taken over 150 u either side of the point (span), so it turns through a corner instead of flipping at the waypoint. Test: tools\lua\lua.exe scripts\lib\lane_geom\selftest.lua from the repo root.

Source

The code of 1.0.0 as the client gets it. The bundle's bytecode is built from these files.

init.lua145 lines
--- the lane as a polyline: where a point stands on it, which way it runs there, and how far a
--- creep walks along it. one copy of what awareness, naga, natureprophet and clockwerk each
--- carried as "nearest waypoint, then straight to the next", which cuts every corner.
---@class lane_geom
local M = {}

local sqrt, atan2 = math.sqrt, math.atan2 or function(y, x) return math.atan(y, x) end

local function xy(p)
    if p.GetX then return p:GetX(), p:GetY() end
    return p.x, p.y
end

-- every waypoint as plain numbers, once per list: the callers hand over the same table each frame
local cache = setmetatable({}, { __mode = "k" })
local function flat(wps)
    local f = cache[wps]
    if f then return f end
    f = { n = #wps, x = {}, y = {}, s = {} }
    local acc = 0
    for i = 1, #wps do
        f.x[i], f.y[i] = xy(wps[i])
        if i > 1 then
            local dx, dy = f.x[i] - f.x[i - 1], f.y[i] - f.y[i - 1]
            acc = acc + sqrt(dx * dx + dy * dy)
        end
        f.s[i] = acc
    end
    f.len = acc
    cache[wps] = f
    return f
end

-- arc length (from waypoint 1) of the point on the polyline closest to (px, py), and how far off
local function project(f, px, py)
    local best_s, best_d2 = 0, math.huge
    for i = 1, f.n - 1 do
        local ax, ay = f.x[i], f.y[i]
        local vx, vy = f.x[i + 1] - ax, f.y[i + 1] - ay
        local l2 = vx * vx + vy * vy
        local t = 0
        if l2 > 0 then
            t = ((px - ax) * vx + (py - ay) * vy) / l2
            if t < 0 then t = 0 elseif t > 1 then t = 1 end
        end
        local qx, qy = ax + vx * t, ay + vy * t
        local d2 = (px - qx) ^ 2 + (py - qy) ^ 2
        if d2 < best_d2 then best_d2, best_s = d2, f.s[i] + t * sqrt(l2) end
    end
    return best_s, sqrt(best_d2)
end

-- the point at arc length s (clamped to the ends)
local function at(f, s)
    if s <= 0 then return f.x[1], f.y[1] end
    if s >= f.len then return f.x[f.n], f.y[f.n] end
    for i = 1, f.n - 1 do
        if s <= f.s[i + 1] then
            local l = f.s[i + 1] - f.s[i]
            local t = l > 0 and (s - f.s[i]) / l or 0
            return f.x[i] + (f.x[i + 1] - f.x[i]) * t, f.y[i] + (f.y[i + 1] - f.y[i]) * t
        end
    end
    return f.x[f.n], f.y[f.n]
end

--- arc length along the lane of the point nearest to `pos`, and its distance from the lane.
---@param wps table[] waypoints in index order
---@param pos table a point with GetX/GetY or x/y
---@return number s
---@return number off
function M.project(wps, pos)
    if not wps or #wps < 2 or not pos then return 0, math.huge end
    local px, py = xy(pos)
    return project(flat(wps), px, py)
end

--- unit heading of the lane at `pos`, going the way `dir` says (+1 = rising waypoint index,
--- -1 = falling). taken over `span` units either side of the point, so a corner turns the
--- arrow over `span` rather than flipping it at the waypoint. nil for a lane of one point.
---@param wps table[]
---@param pos table
---@param dir integer
---@param span? number default 150
---@return number|nil dx
---@return number|nil dy
function M.heading(wps, pos, dir, span)
    if not wps or #wps < 2 or not pos then return nil end
    local f = flat(wps)
    local px, py = xy(pos)
    local s = project(f, px, py)
    span = span or 150
    local ax, ay = at(f, s - span)
    local bx, by = at(f, s + span)
    local dx, dy = bx - ax, by - ay
    local len = sqrt(dx * dx + dy * dy)
    if len < 1 then return nil end
    if dir < 0 then dx, dy = -dx, -dy end
    return dx / len, dy / len
end

--- where a creep that stands at `pos` is after walking `dist` along the lane: projected on
--- the lane first, then along it, corners included. stops at the lane's end.
---@param wps table[]
---@param pos table
---@param dist number
---@param dir integer +1 rising waypoint index, -1 falling
---@return number x
---@return number y
function M.walk(wps, pos, dist, dir)
    local px, py = xy(pos)
    if not wps or #wps < 2 then return px, py end
    local f = flat(wps)
    local s = project(f, px, py)
    return at(f, s + (dir < 0 and -dist or dist))
end

--- `walk` as a Vector (z of the start point kept), for callers that keep Vectors.
---@param wps table[]
---@param pos table
---@param dist number
---@param dir integer
---@return table
function M.walk_point(wps, pos, dist, dir)
    local x, y = M.walk(wps, pos, dist, dir)
    local z = pos.GetZ and pos:GetZ() or pos.z or 0
    local V = rawget(_G, "Vector")
    if V then return V(x, y, z) end
    return { x = x, y = y, z = z }
end

--- heading as an angle in world axes, radians (atan2(dy, dx)); nil as `heading`.
---@param wps table[]
---@param pos table
---@param dir integer
---@param span? number default 150
---@return number|nil
function M.angle(wps, pos, dir, span)
    local dx, dy = M.heading(wps, pos, dir, span)
    if not dx then return nil end
    return atan2(dy, dx)
end

return M
pack.json2 lines
{ "kind": "lib", "version": "1.0.0" }
Changelog

No release notes recorded for this listing.

Used by

No published script needs it yet.

More from this author

More from this author

ground

by hunnyc

Ground height and high-ground level at any point of the dota map.

pack.json
"requires": { "ground": "^1.0" }
your lua
local ground = require("lib/ground")
0 scripts use itNo published version yet

Beta Tester Config

by hunnyc
Included with access

Beta 0.10.54 preset: combo on F, last hit on B, every other bind moved off dota's own keys, hp cards and minimap marks on.

configcombolast hit
Not rated yet0 installs
v1.0.0 · Updated 2026-10-17✓ 0.10.70

fog_seen

by hunnyc

Where each enemy hero is while nobody sees him: the tick he left vision, his last cast in the fog, a teleport's landing, a position another script worked out, plus the enemy smokes.

pack.json
"requires": { "fog_seen": "^1.0" }
your lua
local fog_seen = require("lib/fog_seen")
0 scripts use itv1.0.1