diff --git a/FS19_AutoDrive/scripts/AutoDrive.lua b/FS19_AutoDrive/scripts/AutoDrive.lua index ae974a4f..e1234446 100644 --- a/FS19_AutoDrive/scripts/AutoDrive.lua +++ b/FS19_AutoDrive/scripts/AutoDrive.lua @@ -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 @@ -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) @@ -246,7 +253,7 @@ function AutoDrive:keyEvent(unicode, sym, modifier, isDown) AutoDrive.enableSphrere = true else AutoDrive.enableSphrere = AutoDrive.toggleSphrere - end + end end end diff --git a/FS19_AutoDrive/scripts/Hud.lua b/FS19_AutoDrive/scripts/Hud.lua index 320a98f5..40fc3a32 100644 --- a/FS19_AutoDrive/scripts/Hud.lua +++ b/FS19_AutoDrive/scripts/Hud.lua @@ -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 diff --git a/FS19_AutoDrive/scripts/Manager/GraphManager.lua b/FS19_AutoDrive/scripts/Manager/GraphManager.lua index c1535c54..bfbd4ff2 100644 --- a/FS19_AutoDrive/scripts/Manager/GraphManager.lua +++ b/FS19_AutoDrive/scripts/Manager/GraphManager.lua @@ -7,6 +7,7 @@ function ADGraphManager:load() self.groups["All"] = 1 self.changes = false self.preparedWayPoints = false + self.curvePreview = nil end function ADGraphManager:markChanges() @@ -503,9 +504,41 @@ 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 @@ -513,6 +546,196 @@ function ADGraphManager:toggleConnectionBetween(startNode, endNode, reverseDirec 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 diff --git a/FS19_AutoDrive/scripts/Specialization.lua b/FS19_AutoDrive/scripts/Specialization.lua index 99c9a242..1a5d3bd6 100644 --- a/FS19_AutoDrive/scripts/Specialization.lua +++ b/FS19_AutoDrive/scripts/Specialization.lua @@ -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() diff --git a/FS19_AutoDrive/scripts/Utils/UtilFuncs.lua b/FS19_AutoDrive/scripts/Utils/UtilFuncs.lua index 6ee59acf..5ba4bf23 100644 --- a/FS19_AutoDrive/scripts/Utils/UtilFuncs.lua +++ b/FS19_AutoDrive/scripts/Utils/UtilFuncs.lua @@ -76,7 +76,7 @@ function AutoDrive:getTerrainHeightAtWorldPos(x, z) -- get a starting height with the basic function local y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 1, z) -- do a raycast from a bit above y - raycastClosest(x, y + 9, z, 0, -1, 0, "getTerrainHeightAtWorldPos_Callback", 10, self, 12) + raycastClosest(x, y + 2, z, 0, -1, 0, "getTerrainHeightAtWorldPos_Callback", 3, self, 12) return self.raycastHeight or y end @@ -709,6 +709,11 @@ function AutoDrive:getIsActivatable(superFunc, objectToFill) end function AutoDrive:zoomSmoothly(superFunc, offset) + -- if smooth curve preview is active, use mousewheel to increase and decrease smoothness and prevent zooming + if AutoDrive.experimentalFeatures.smoothWaypointConnection and ADGraphManager.curvePreview and AutoDrive.leftLSHIFTmodifierKeyPressed then + ADGraphManager:recalculatePreview(offset / 3) + return + end if not AutoDrive.mouseWheelActive then -- don't zoom camera when mouse wheel is used to scroll targets (thanks to sperrgebiet) superFunc(self, offset) end @@ -962,4 +967,77 @@ function AutoDrive.checkWaypointsMultipleSameOut(correctit) end end +ADVectorUtils = {} + +--- Calculates the unit vector on a given vector. +--- @param vector table Table with x and z properties. +--- @return table Unitvector as table with x and z properties. +function ADVectorUtils.unitVector2D(vector) + local x, z = vector.x or 0, vector.z or 0 + local q = math.sqrt( (x * x) + ( z * z ) ) + return {x = x / q, z = z / q} +end + +--- Scales a vector by a given scalar. +--- @param vector table Table with x and z properties. +--- @param scale number Scale +--- @return table Vector +function ADVectorUtils.scaleVector2D(vector, scale) + scale = scale or 1.0 + vector.x = ( vector.x * scale ) or 0 + vector.z = ( vector.z * scale ) or 0 + return vector +end + +--- Returns the distance between zwo vectors using their x and z coordinates. +--- @param vectorA table with x and z property +--- @param vectorB table with x and z property +--- @return number Distance between vectorA and vectorB +function ADVectorUtils.distance2D(vectorA, vectorB) + return MathUtil.vector2Length(vectorA.x - vectorB.x, vectorA.z - vectorB.z) +end + +--- Returns a new vector pointing from vectorA to vectorB by subtracting A from B +--- @param vectorA table with x and z property +--- @param vectorB table with x and z property +--- @return table Vector pointing from A to B +function ADVectorUtils.subtract2D(vectorA, vectorB) + return {x = vectorB.x - vectorA.x, z = vectorB.z - vectorA.z} +end + +--- Inverts a vector with x and z properties. +--- @param vector table with x and z property +--- @return table Vector inverted +function ADVectorUtils.invert2D(vector) + vector.x = vector.x * -1 + vector.z = vector.z * -1 + return vector +end + +--- Adds x and z values of two given vectors and returns a new vector with x and z properties. +--- @param vectorA table with x and z property +--- @param vectorB table with x and z property +--- @return table Vector +function ADVectorUtils.add2D(vectorA, vectorB) + return {x = vectorA.x + vectorB.x, z = vectorA.z + vectorB.z} +end + +--- Does a linear interpolation based on the in* range and value, and returns a new value +--- fitting in the out range. Second return value is the interpolated value between 0..1. +--- @param inMin number Minimum value for input range +--- @param inMax number Maximum number for input range +--- @param inValue number Current value for input range +--- @param outMin number Minimum value vor output range +--- @param outMax number Maximum value for output range +--- @return number, number Interpolated value in output range, value in range 0..1 +function ADVectorUtils.linterp(inMin, inMax, inValue, outMin, outMax) + -- normalize input, make min range boundary = 0, nval is between 0..1 + local imax = inMax - inMin + local nval = math.clamp(0, inValue - inMin, imax) / imax + -- normalize output + local omax = outMax - outMin + local oval = outMin + ( omax * nval ) + return oval, nval +end + -- TODO: Maybe we should add a console command that allows to run console commands to server