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

Feature curve creation #1802

Open
wants to merge 8 commits into
base: beta-testing
Choose a base branch
from
15 changes: 11 additions & 4 deletions FS19_AutoDrive/scripts/AutoDrive.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ AutoDrive.directory = g_currentModDirectory
g_autoDriveUIFilename = AutoDrive.directory .. "textures/GUI_Icons.dds"
g_autoDriveDebugUIFilename = AutoDrive.directory .. "textures/gui_debug_Icons.dds"

AutoDrive.experimentalFeatures = {}
AutoDrive.experimentalFeatures.redLinePosition = false
AutoDrive.experimentalFeatures.dynamicChaseDistance = false
AutoDrive.experimentalFeatures = {
redLinePosition = false,
dynamicChaseDistance = false,
smoothWaypointConnection = false
}

AutoDrive.smootherDriving = true
AutoDrive.developmentControls = false
Expand All @@ -18,6 +20,11 @@ AutoDrive.mapHotspotsBuffer = {}
AutoDrive.drawHeight = 0.3
AutoDrive.drawDistance = getViewDistanceCoeff() * 50

-- Global set of colors used in rendering
AutoDrive.COLORS = {
EDITOR_LINE_PREVIEW = {r=1, g=0.85, b=0, a=1}
}

AutoDrive.STAT_NAMES = {"driversTraveledDistance", "driversHired"}
for _, statName in pairs(AutoDrive.STAT_NAMES) do
table.insert(FarmStats.STAT_NAMES, statName)
Expand Down Expand Up @@ -246,7 +253,7 @@ function AutoDrive:keyEvent(unicode, sym, modifier, isDown)
AutoDrive.enableSphrere = true
else
AutoDrive.enableSphrere = AutoDrive.toggleSphrere
end
end
end
end

Expand Down
37 changes: 29 additions & 8 deletions FS19_AutoDrive/scripts/Hud.lua
Original file line number Diff line number Diff line change
Expand Up @@ -409,30 +409,51 @@ function AutoDriveHud:mouseEvent(vehicle, posX, posY, isDown, isUp, button)
-- 1st or 2nd Editor Mode enabled
-- try to get a waypoint in mouse range
for _, point in pairs(vehicle:getWayPointsInRange(0, AutoDrive.drawDistance)) do
if AutoDrive.mouseIsAtPos(point, 0.01) then
vehicle.ad.hoveredNodeId = point.id
if AutoDrive.mouseIsAtPos(point, 0.01) then
if point.id ~= vehicle.ad.hoveredNodeId then
vehicle.ad.hoveredNodeId = point.id

-- if there is a selected node which is not the hovered node, draw a preview of a potential connection
if AutoDrive.experimentalFeatures.smoothWaypointConnection and
vehicle.ad.selectedNodeId ~= nil and vehicle.ad.selectedNodeId ~= vehicle.ad.hoveredNodeId then
ADGraphManager:previewConnectionBetween(
ADGraphManager:getWayPointById(vehicle.ad.selectedNodeId),
ADGraphManager:getWayPointById(vehicle.ad.hoveredNodeId)
)
end
end
break
end
end
if vehicle.ad.nodeToMoveId ~= nil then
-- move point at mouse position
AutoDrive.moveNodeToMousePos(vehicle.ad.nodeToMoveId)
AutoDrive.moveNodeToMousePos(vehicle.ad.nodeToMoveId)

if AutoDrive.experimentalFeatures.smoothWaypointConnection and
ADGraphManager.curvePreview ~= nil and ADGraphManager:doesNodeAffectPreview(vehicle.ad.nodeToMoveId) then
-- recalculate curve
ADGraphManager:previewConnectionBetween(
ADGraphManager.curvePreview.startNode,
ADGraphManager.curvePreview.endNode
)
end
end
if vehicle.ad.hoveredNodeId ~= nil then
-- waypoint at mouse position
if button == 1 and isUp and not AutoDrive.leftALTmodifierKeyPressed and not AutoDrive.leftCTRLmodifierKeyPressed then
-- left mouse button to select point / connect to already selected point
if vehicle.ad.selectedNodeId ~= nil then
if vehicle.ad.selectedNodeId ~= vehicle.ad.hoveredNodeId then
-- connect selected point with hovered point
ADGraphManager:toggleConnectionBetween(ADGraphManager:getWayPointById(vehicle.ad.selectedNodeId), ADGraphManager:getWayPointById(vehicle.ad.hoveredNodeId), AutoDrive.leftLSHIFTmodifierKeyPressed)
if vehicle.ad.selectedNodeId ~= vehicle.ad.hoveredNodeId then
ADGraphManager:toggleConnectionBetween(ADGraphManager:getWayPointById(vehicle.ad.selectedNodeId), ADGraphManager:getWayPointById(vehicle.ad.hoveredNodeId), AutoDrive.leftLSHIFTmodifierKeyPressed)
end
-- unselect point
vehicle.ad.selectedNodeId = nil
vehicle.ad.selectedNodeId = nil
ADGraphManager.curvePreview = nil
else
-- select point
-- no selectedNodeId: hoveredNodeId is now selectedNodeId
vehicle.ad.selectedNodeId = vehicle.ad.hoveredNodeId
vehicle.ad.selectedNodeId = vehicle.ad.hoveredNodeId
ADGraphManager.curvePreview = nil
end
end

Expand Down
229 changes: 226 additions & 3 deletions FS19_AutoDrive/scripts/Manager/GraphManager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function ADGraphManager:load()
self.groups["All"] = 1
self.changes = false
self.preparedWayPoints = false
self.curvePreview = nil
end

function ADGraphManager:markChanges()
Expand Down Expand Up @@ -503,16 +504,238 @@ function ADGraphManager:toggleConnectionBetween(startNode, endNode, reverseDirec
table.removeValue(startNode.out, endNode.id)
table.removeValue(endNode.incoming, startNode.id)
else
table.insert(startNode.out, endNode.id)
if not reverseDirection then
table.insert(endNode.incoming, startNode.id)
-- if there is an active preview between startNode and endNode use this to create new WPs - otherwise just create a straight line
if AutoDrive.experimentalFeatures.smoothWaypointConnection and
self.curvePreview ~= nil and startNode == self.curvePreview.startNode and endNode == self.curvePreview.endNode then

-- if there are only 2 WP in the preview - just create a straight line as before
if #self.curvePreview.waypoints == 2 then
table.insert(startNode.out, endNode.id)
if not reverseDirection then
table.insert(endNode.incoming, startNode.id)
end
self:markChanges()
return
end

local aWP = startNode
local bWP = nil

-- ommit start and endnode in the list
for i = 2, #self.curvePreview.waypoints - 1 do
local p = self.curvePreview.waypoints[i]
self:createWayPoint(p.x, p.y, p.z)
-- HACK: we assume that we created the last WP here, ignoring MP and parallel events... :(
bWP = self:getWayPointById(self:getWayPointsCount())

ADGraphManager:toggleConnectionBetween(aWP, bWP, false)
aWP, bWP = bWP, nil
end
-- connect last new WP with end node
ADGraphManager:toggleConnectionBetween(aWP, endNode, false)
else
-- original behaviour - just create a straight line
table.insert(startNode.out, endNode.id)
if not reverseDirection then
table.insert(endNode.incoming, startNode.id)
end
end
end

self:markChanges()
end
end

--- Checks if a given node is part of the current preview.
--- Will be used to decide if the preview need to be refreshed, e.g. if the node has been moved.
--- @param nodeId integer Id of the node to check
--- @return boolean true if the node is used in preview calculation
function ADGraphManager:doesNodeAffectPreview(nodeId)
if self.curvePreview == nil then
return false
end
if self.curvePreview.startNode.id == nodeId or
self.curvePreview.endNode.id == nodeId or
self.curvePreview.p0.id == nodeId or
self.curvePreview.p3.id == nodeId then
return true
end
return false
end

--- Enforces a recalculation of the smooth preview, e.g. if the curvature has changed.
--- @param curvature number Roundness of the curve. Delta that is used to increase or decrease the roundness
function ADGraphManager:recalculatePreview(curvature)
if self.curvePreview == nil then
return
end
curvature = curvature or 0
-- if the curvature is already at it's min value and we still try to decrease is - fallback to a straight line
if curvature < 0 and self.curvePreview.curvature <= self.curvePreview.MIN_CURVATURE then
self.curvePreview.curvature = -1
else
self.curvePreview.curvature = math.clamp(
self.curvePreview.MIN_CURVATURE, self.curvePreview.curvature + curvature, self.curvePreview.MAX_CURVATURE
)
end
self:previewConnectionBetween(self.curvePreview.startNode, self.curvePreview.endNode, nil, true)
end

--- Create a smooth connection between two nodes by applying a RomCutmull smoothing algorithm.
--- @param startNode table Node to start the curve with
--- @param endNode table Node to end the curve at
--- @param reverseDirection boolean If true, make a curve driving reverse
function ADGraphManager:previewConnectionBetween(startNode, endNode, reverseDirection, reuseParams)
if startNode == nil or endNode == nil then
return
end

if self.curvePreview ~= nil and startNode == self.curvePreview.startNode and endNode == self.curvePreview.endNode then
reuseParams = true
end

if table.contains(startNode.out, endNode.id) or table.contains(endNode.incoming, startNode.id) then
-- nodes are already connected - do not create preview
return
end

-- TODO: if we have more then one inbound or outbound connections, get the avg. vector
if #startNode.incoming == 1 and #endNode.out == 1 then

if reuseParams == nil or reuseParams == false then
self.curvePreview = {
MIN_CURVATURE = 0.5,
MAX_CURVATURE = 3.5,
startNode = startNode,
p0 = nil,
endNode = endNode,
p3 = nil,
curvature = 1,
waypoints = { startNode }
}
else
-- fallback to straight line
if self.curvePreview.curvature <= 0 then
self.curvePreview.waypoints = { startNode, endNode }
return
end
self.curvePreview.waypoints = { startNode }
end

local p0 = nil
for _, px in pairs(startNode.incoming) do
p0 = ADGraphManager:getWayPointById(px)
self.curvePreview.p0 = p0
break
end
local p3 = nil
for _, px in pairs(endNode.out) do
p3 = ADGraphManager:getWayPointById(px)
self.curvePreview.p3 = p3
break
end

if reuseParams == nil or reuseParams == false then
-- calculate the angle between start tangent and end tangent
local dAngle = math.abs(AutoDrive.angleBetween(
ADVectorUtils.subtract2D(p0, startNode),
ADVectorUtils.subtract2D(endNode, p3)
))
self.curvePreview.curvature = ADVectorUtils.linterp(0, 180, dAngle, 1.5, 2.5)
end

-- distance from start to end, divided by two to give it more roundness...
local dStartEnd = ADVectorUtils.distance2D(startNode, endNode) / self.curvePreview.curvature

-- we need to normalize the length of p0-start and end-p3, otherwise their length will influence the curve
-- get vector from p0->start
local vp0Start = ADVectorUtils.subtract2D(p0, startNode)
-- calculate unit vector of vp0Start
vp0Start = ADVectorUtils.unitVector2D(vp0Start)
-- scale it like start->end
vp0Start = ADVectorUtils.scaleVector2D(vp0Start, dStartEnd)
-- invert it
vp0Start = ADVectorUtils.invert2D(vp0Start)
-- add it to the start Vector so that we get new p0
p0 = ADVectorUtils.add2D(startNode, vp0Start)
-- make sure p0 has a y value
p0.y = startNode.y

-- same for end->p3, except that we do not need to invert it, but just add it to the endNode
local vEndp3 = ADVectorUtils.subtract2D(endNode, p3)
vEndp3 = ADVectorUtils.unitVector2D(vEndp3)
vEndp3 = ADVectorUtils.scaleVector2D(vEndp3, dStartEnd)
p3 = ADVectorUtils.add2D(endNode, vEndp3)
p3.y = endNode.y

local prevWP = startNode
local prevV = ADVectorUtils.subtract2D(p0, startNode)
-- we're calculting a VERY smooth curve and whenever the new point on the curve has a good distance to the last one create a new waypoint
-- but make sure that the last point also has a good distance to the endNode
for i = 1, 200 do
local px = ADGraphManager:CatmullRomInterpolate(i, p0, startNode, endNode, p3, 200)
local newV = ADVectorUtils.subtract2D(prevWP, px)
local dAngle = math.abs(AutoDrive.angleBetween(prevV, newV))

-- only create new WP if distance to last one is > 1.5m and distance to target > 1.5m and angle to last one > 3°
-- or at least every 4m
local distPrev = ADVectorUtils.distance2D(prevWP, px)
local distEnd = ADVectorUtils.distance2D(px, endNode)
if ( distPrev >= 1.5 and distEnd >= 1.5 and dAngle >= 3 ) or (distPrev >= 4 and distEnd >= 4) then
-- get height at terrain
px.y = AutoDrive:getTerrainHeightAtWorldPos(px.x, px.z)
table.insert(self.curvePreview.waypoints, px)
prevWP = px -- newWP
prevV = newV
end
end
table.insert(self.curvePreview.waypoints, endNode)

else -- fallback to straight line connection behaviour
return
end
end


function ADGraphManager:CatmullRomInterpolate(index, p0, p1, p2, p3, segments)
local px = {x=nil, y=nil, z=nil}
local x = {p0.x, p1.x, p2.x, p3.x}
local z = {p0.z, p1.z, p2.z, p3.z}
local time = {0, 1, 2, 3} -- linear at start... calculate weights over time
local total = 0.0

for i = 2, 4 do
local dx = x[i] - x[i - 1]
local dz = z[i] - z[i - 1]
-- the .9 is giving the wideness and roundness of the curve,
-- lower values (like .25 will be more straight, while high values like .95 will be wider and rounder)
total = total + math.pow(dx * dx + dz * dz, 0.95)
time[i] = total
end
local tstart = time[2]
local tend = time[3]
local t = tstart + (index * (tend - tstart)) / segments

local L01 = p0.x * (time[2] - t) / (time[2] - time[1]) + p1.x * (t - time[1]) / (time[2] - time[1])
local L12 = p1.x * (time[3] - t) / (time[3] - time[2]) + p2.x * (t - time[2]) / (time[3] - time[2])
local L23 = p2.x * (time[4] - t) / (time[4] - time[3]) + p3.x * (t - time[3]) / (time[4] - time[3])
local L012 = L01 * (time[3] - t) / (time[3] - time[1]) + L12 * (t - time[1]) / (time[3] - time[1])
local L123 = L12 * (time[4] - t) / (time[4] - time[2]) + L23 * (t - time[2]) / (time[4] - time[2])
local C12 = L012 * (time[3] - t) / (time[3] - time[2]) + L123 * (t - time[2]) / (time[3] - time[2])
px.x = C12

L01 = p0.z * (time[2] - t) / (time[2] - time[1]) + p1.z * (t - time[1]) / (time[2] - time[1])
L12 = p1.z * (time[3] - t) / (time[3] - time[2]) + p2.z * (t - time[2]) / (time[3] - time[2])
L23 = p2.z * (time[4] - t) / (time[4] - time[3]) + p3.z * (t - time[3]) / (time[4] - time[3])
L012 = L01 * (time[3] - t) / (time[3] - time[1]) + L12 * (t - time[1]) / (time[3] - time[1])
L123 = L12 * (time[4] - t) / (time[4] - time[2]) + L23 * (t - time[2]) / (time[4] - time[2])
C12 = L012 * (time[3] - t) / (time[3] - time[2]) + L123 * (t - time[2]) / (time[3] - time[2])
px.z = C12

return px
end


function ADGraphManager:createWayPoint(x, y, z, sendEvent)
if sendEvent == nil or sendEvent == true then
-- Propagating waypoint creation all over the network
Expand Down
12 changes: 12 additions & 0 deletions FS19_AutoDrive/scripts/Specialization.lua
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,18 @@ function AutoDrive:onDrawEditorMode()
DrawingManager:addCrossTask(x, y, z)
end
end

-- draw curve preview if available
if AutoDrive.experimentalFeatures.smoothWaypointConnection and
AutoDrive.isInExtendedEditorMode() and ADGraphManager.curvePreview ~= nil then
local col = AutoDrive.COLORS.EDITOR_LINE_PREVIEW
for i = 1, #ADGraphManager.curvePreview.waypoints - 1, 1 do
local p0 = ADGraphManager.curvePreview.waypoints[i]
local p1 = ADGraphManager.curvePreview.waypoints[i + 1]
DrawingManager:addLineTask(p0.x, p0.y, p0.z, p1.x, p1.y, p1.z, col.r, col.g, col.b)
DrawingManager:addSphereTask(p1.x, p1.y, p1.z, 2.0, col.r, col.g, col.b, col.a)
end
end
end

function AutoDrive:startAutoDrive()
Expand Down
Loading