Editor tool for drawing road networks in the scene. Handles cursor picking, snapping, angle snapping and alignment, splitting/branching segments, creating nodes and segments, and draws preview gizmos; includes a sidebar widget for tool properties and shortcuts.
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;
namespace RedSnail.RoadTool2.Editor;
/// <summary>
/// Click on the ground to lay road nodes; consecutive clicks are joined by segments. Clicking an existing node
/// continues from it or joins the road to it; clicking on an existing road splits it there and does the same, which
/// is how roads get branched off and merged into each other. Right-click or Escape ends the chain.
/// </summary>
[Title("Draw")]
[Icon("timeline")]
[Group("1")]
[Order(0)]
public class RoadDrawTool : EditorTool
{
/// <summary>
/// A right button release this close to where it was pressed is a click; anything further is the camera orbiting.
/// </summary>
private const float ClickDragTolerance = 4.0f;
/// <summary>
/// A split this close to a road's end reuses the end node instead of leaving a sliver of road behind.
/// </summary>
private const float MinEndDistance = 25.0f;
/// <summary>
/// What the cursor is over this frame and where the next node would go.
/// </summary>
private readonly struct Target
{
public readonly Vector3 World;
public readonly RoadNode Node;
public readonly RoadSegmentPick? Road;
/// <summary>
/// The parking lot the point opens into, when the cursor is over one: the road starts or ends on its edge.
/// </summary>
public readonly RoadLot Lot;
public Target(Vector3 _World, RoadNode _Node, RoadSegmentPick? _Road, RoadLot _Lot = null)
{
World = _World;
Node = _Node;
Road = _Road;
Lot = _Lot;
}
}
[Property, Title("Width"), Range(50.0f, 2000.0f)] public float Width { get; set; } = 500.0f;
/// <summary>
/// The road type new roads get. Empty uses the network's default profile.
/// </summary>
[Property, Title("Profile")] public RoadProfile Profile { get; set; }
/// <summary>
/// Set, new roads are drawn as bridges in this style. The Edit tool changes it per road afterwards.
/// </summary>
[Property, Title("Bridge")] public BridgeStyle Bridge { get; set; }
/// <summary>
/// What new roads get standing along them (lamp posts, trees...). The Edit tool changes it per road afterwards.
/// </summary>
[Property, Title("Props")] public RoadPropSet Props { get; set; }
[Property, Title("Snap Radius"), Range(0.0f, 500.0f)] public float SnapRadius { get; set; } = 150.0f;
/// <summary>
/// Lanes new roads get: two-way with the odd lane running the way the road is drawn, or one-way that way.
/// The Lanes tool changes them afterwards.
/// </summary>
[Property, Title("Lanes"), Range(RoadSegment.MinLanes, RoadSegment.MaxLanes)] public int Lanes { get; set { field = value.Clamp(RoadSegment.MinLanes, RoadSegment.MaxLanes); } } = 2;
[Property, Title("One Way")] public bool OneWay { get; set; } = false;
/// <summary>
/// Sharp corners give straight roads meeting at an angle (grid streets); off, the road flows smoothly through
/// every node placed.
/// </summary>
[Property, Title("Sharp Corners")] public bool SharpCorners { get; set; } = false;
/// <summary>
/// Snap the direction of the road being drawn to whole steps of <see cref="AngleStep"/>, measured from the
/// world axes and from the roads already at the node being extended. Holding Shift flips it for one placement.
/// </summary>
[Property, Title("Angle Snap")] public bool AngleSnap { get; set; } = false;
[Property, Title("Snap Angle"), Range(1.0f, 90.0f)] public float AngleStep { get; set; } = 10.0f;
/// <summary>
/// How far the snapped direction is drawn out as a guide while placing.
/// </summary>
private const float GuideLength = 4000.0f;
private int m_LastNodeId = -1;
private Vector2 m_RightPressPosition;
/// <summary>
/// The angle the current placement snapped to, for the guide; null when not snapping.
/// </summary>
private float? m_SnappedAngle;
/// <summary>
/// The node the current placement is aligned with, for the guide; null when not aligned.
/// </summary>
private Vector3? m_AlignAnchor;
private RoadNetworkTool Root => Manager?.CurrentTool as RoadNetworkTool;
public bool IsDrawing => m_LastNodeId >= 0;
private bool SnapActive => AngleSnap != Gizmo.IsShiftPressed;
public override void OnDisabled()
{
EndChain();
}
public override Widget CreateToolSidebar()
{
return new RoadDrawToolWidget(this);
}
public override void OnUpdate()
{
var root = Root;
if (root is null)
return;
var network = root.Network;
// Undo can pull the node we were extending from out from under us.
if (IsDrawing && (!network.IsValid() || network.FindNode(m_LastNodeId) is null))
EndChain();
if (!Gizmo.HasMouseFocus)
return;
if (Gizmo.WasRightMousePressed)
m_RightPressPosition = Gizmo.CursorPosition;
if (Gizmo.WasRightMouseReleased && Vector2.Distance(Gizmo.CursorPosition, m_RightPressPosition) < ClickDragTolerance)
EndChain();
if (!TryGetCursorPosition(network, out Vector3 cursorWorld))
return;
var target = ResolveTarget(network, cursorWorld);
m_SnappedAngle = null;
m_AlignAnchor = null;
// Joining a node is explicit and wins. Otherwise the snap places the end: on bare ground, or on a road the
// snapped line crosses (the hovered road above all), which is split right there so the merge is straight.
if (target.Node is null && IsDrawing && SnapActive && TrySnapAngle(network, cursorWorld, target.Road, out Vector3 snappedWorld, out float angle, out RoadSegmentPick? split))
{
if (split is { } pick)
{
target = new Target(snappedWorld, null, pick);
m_SnappedAngle = angle;
}
else if (target.Road is null)
{
// The snapped end landing on a node joins it, like clicking the node would; landing on a parking
// lot opens into it, where the snapped line crosses its edge.
var joined = network.FindNearestNode(network.WorldTransform.PointToLocal(snappedWorld), SnapRadius);
if (joined is not null && joined.Id != m_LastNodeId)
target = new Target(network.WorldTransform.PointToWorld(joined.Position), joined, null);
else if (LotUnder(network, network.WorldTransform.PointToLocal(snappedWorld)) is { } snappedLot)
target = new Target(EdgeWorld(network, snappedLot, snappedWorld), null, null, snappedLot);
else
target = new Target(snappedWorld, null, null);
m_SnappedAngle = angle;
}
else
{
// Hovering a road the snapped line never reaches: the plain join, and no alignment guide.
m_AlignAnchor = null;
}
}
DrawPreview(network, target);
if (Gizmo.WasLeftMousePressed)
PlaceNode(root, target);
}
/// <summary>
/// Snaps the direction from the node being extended to the cursor onto the nearest of the snap angles: whole
/// steps round from the world X axis, and whole steps round from each road already leaving that node (so a
/// branch can be exactly square to, or straight on from, a road at any angle). The cursor's distance along the
/// snapped direction is kept and the point is dropped back onto the ground there.
/// </summary>
private bool TrySnapAngle(RoadNetwork _Network, Vector3 _CursorWorld, RoadSegmentPick? _Hovered, out Vector3 _SnappedWorld, out float _Angle, out RoadSegmentPick? _Split)
{
_SnappedWorld = _CursorWorld;
_Angle = 0.0f;
_Split = null;
if (!_Network.IsValid() || _Network.FindNode(m_LastNodeId) is not { } last)
return false;
var transform = _Network.WorldTransform;
Vector3 startWorld = transform.PointToWorld(last.Position);
var delta = new Vector2(_CursorWorld.x - startWorld.x, _CursorWorld.y - startWorld.y);
if (delta.Length < 1.0f)
return false;
float step = MathF.Max(1.0f, AngleStep);
float raw = MathF.Atan2(delta.y, delta.x).RadianToDegree();
float best = SnapTo(raw, 0.0f, step);
foreach (var segment in _Network.GetSegmentsAt(last.Id))
{
if (!_Network.TryGetCurve(segment, out var curve))
continue;
// The road's direction leaving this node, and straight on through it.
Vector3 tangent = segment.StartNodeId == last.Id ? curve.Tangent(0.0f) : -curve.Tangent(1.0f);
Vector3 worldTangent = transform.NormalToWorld(tangent);
var flat = new Vector2(worldTangent.x, worldTangent.y);
if (flat.Length < 0.001f)
continue;
float reference = MathF.Atan2(flat.y, flat.x).RadianToDegree();
foreach (float candidate in new[] { SnapTo(raw, reference, step), SnapTo(raw, reference + 180.0f, step) })
{
if (MathF.Abs(AngleDifference(candidate, raw)) < MathF.Abs(AngleDifference(best, raw)))
best = candidate;
}
}
var direction = new Vector2(MathF.Cos(best.DegreeToRadian()), MathF.Sin(best.DegreeToRadian()));
var start = new Vector2(startWorld.x, startWorld.y);
float length = MathF.Max(1.0f, Vector2.Dot(delta, direction));
float tolerance = AlignTolerance(_CursorWorld);
_Angle = NormalizeAngle(best);
// A road the snapped line crosses near the end splits there: the merge comes out dead straight. The road
// under the cursor is what the user is aiming at, so it takes the crossing from much further off.
if (TryCrossRoad(_Network, start, direction, length, tolerance, _Hovered, out RoadSegmentPick pick, out Vector3 crossingWorld))
{
_SnappedWorld = crossingWorld;
_Split = pick;
// Lining up with a node as well is only a coincidence here; the guide would mislead.
m_AlignAnchor = null;
return true;
}
if (TryAlign(_Network, last, start, direction, length, tolerance, out float aligned, out Vector3 anchor))
{
length = aligned;
m_AlignAnchor = anchor;
}
Vector2 snapped = start + direction * length;
_SnappedWorld = new Vector3(snapped.x, snapped.y, GroundHeightAt(snapped, _CursorWorld.z));
return true;
}
/// <summary>
/// Where the snapped line crosses a road's centreline, when that crossing is within the tolerance of the
/// cursor's distance along the line, or, for the road under the cursor, within a couple of road widths of it.
/// The nearest such crossing wins.
/// </summary>
private static bool TryCrossRoad(RoadNetwork _Network, Vector2 _Start, Vector2 _Direction, float _Length, float _Tolerance, RoadSegmentPick? _Hovered, out RoadSegmentPick _Pick, out Vector3 _World)
{
var transform = _Network.WorldTransform;
float bestError = float.MaxValue;
bool found = false;
_Pick = default;
_World = default;
foreach (var segment in _Network.Segments)
{
var table = _Network.GetCurveTable(segment);
if (table is null)
continue;
bool hovered = _Hovered is { } hover && hover.Segment.Id == segment.Id;
float allowed = hovered ? MathF.Max(_Tolerance, segment.Width * 2.0f) : _Tolerance;
Vector3 previousWorld = transform.PointToWorld(table.SamplePosition(0));
for (int i = 1; i < table.SampleCount; i++)
{
Vector3 currentWorld = transform.PointToWorld(table.SamplePosition(i));
var p = new Vector2(previousWorld.x, previousWorld.y);
var edge = new Vector2(currentWorld.x - previousWorld.x, currentWorld.y - previousWorld.y);
float denominator = Cross(_Direction, edge);
if (MathF.Abs(denominator) > 0.000001f)
{
Vector2 offset = p - _Start;
float t = Cross(offset, edge) / denominator;
float u = Cross(offset, _Direction) / denominator;
// Past the start of the line and within this piece of the road.
if (t >= 1.0f && u >= 0.0f && u <= 1.0f)
{
float error = MathF.Abs(t - _Length);
if (error <= allowed && error < bestError)
{
float distance = table.SampleDistance(i - 1) + u * (table.SampleDistance(i) - table.SampleDistance(i - 1));
float curveT = table.DistanceToT(distance);
bestError = error;
found = true;
_Pick = new RoadSegmentPick(segment, curveT, table.Curve.Evaluate(curveT));
_World = Vector3.Lerp(previousWorld, currentWorld, u);
}
}
}
previousWorld = currentWorld;
}
}
return found;
}
/// <summary>
/// With the direction fixed, the distance along it is the one thing left to place; this snaps it to where the
/// road's line crosses a guide line through another node, when the cursor is close to that crossing. Guides
/// through a node run square to the road being drawn (the end lines up level with that node), along the world
/// axes, and along the roads leaving that node (the end lands on that road's line). Grids come out exact.
/// </summary>
private bool TryAlign(RoadNetwork _Network, RoadNode _Last, Vector2 _Start, Vector2 _Direction, float _Length, float _Tolerance, out float _AlignedLength, out Vector3 _AnchorWorld)
{
var transform = _Network.WorldTransform;
var perpendicular = new Vector2(-_Direction.y, _Direction.x);
float bestError = _Tolerance;
bool found = false;
_AlignedLength = _Length;
_AnchorWorld = default;
foreach (var node in _Network.Nodes)
{
if (node.Id == _Last.Id)
continue;
Vector3 world = transform.PointToWorld(node.Position);
var anchor = new Vector2(world.x, world.y);
foreach (Vector2 guide in GuidesAt(_Network, node, perpendicular))
{
float denominator = Cross(_Direction, guide);
// A guide nearly along the road crosses it nowhere useful.
if (MathF.Abs(denominator) < 0.05f)
continue;
float crossing = Cross(anchor - _Start, guide) / denominator;
if (crossing < 1.0f)
continue;
float error = MathF.Abs(crossing - _Length);
if (error >= bestError)
continue;
bestError = error;
_AlignedLength = crossing;
_AnchorWorld = world;
found = true;
}
}
return found;
}
private static IEnumerable<Vector2> GuidesAt(RoadNetwork _Network, RoadNode _Node, Vector2 _Perpendicular)
{
yield return _Perpendicular;
yield return new Vector2(1.0f, 0.0f);
yield return new Vector2(0.0f, 1.0f);
var transform = _Network.WorldTransform;
foreach (var segment in _Network.GetSegmentsAt(_Node.Id))
{
if (!_Network.TryGetCurve(segment, out var curve))
continue;
Vector3 tangent = segment.StartNodeId == _Node.Id ? curve.Tangent(0.0f) : -curve.Tangent(1.0f);
Vector3 worldTangent = transform.NormalToWorld(tangent);
var flat = new Vector2(worldTangent.x, worldTangent.y);
if (flat.Length > 0.001f)
yield return flat.Normal;
}
}
/// <summary>
/// How close (in the world, at the cursor) the road's end has to be to a guide crossing to take it: a constant
/// on-screen distance, like the node handles.
/// </summary>
private static float AlignTolerance(Vector3 _World)
{
return MathF.Max(25.0f, Gizmo.Camera.Position.Distance(_World) * 0.025f);
}
private static float Cross(Vector2 _A, Vector2 _B)
{
return _A.x * _B.y - _A.y * _B.x;
}
/// <summary>
/// The multiple of the step, counted from the reference angle, nearest to the angle.
/// </summary>
private static float SnapTo(float _Angle, float _Reference, float _Step)
{
return _Reference + MathF.Round(AngleDifference(_Angle, _Reference) / _Step) * _Step;
}
/// <summary>
/// The signed shortest turn from one angle to the other, in (-180, 180].
/// </summary>
private static float AngleDifference(float _To, float _From)
{
float difference = (_To - _From) % 360.0f;
if (difference > 180.0f)
difference -= 360.0f;
else if (difference <= -180.0f)
difference += 360.0f;
return difference;
}
private static float NormalizeAngle(float _Angle)
{
float normalized = _Angle % 360.0f;
return normalized < 0.0f ? normalized + 360.0f : normalized;
}
/// <summary>
/// The ground height at a point in plan, from a trace straight down through it (roads skipped, like the cursor
/// trace); the fallback height when there is nothing there.
/// </summary>
private float GroundHeightAt(Vector2 _Point, float _Fallback)
{
const float reach = 20000.0f;
var top = new Vector3(_Point.x, _Point.y, _Fallback + reach * 0.5f);
var ray = new Ray(top, Vector3.Down);
var physics = Scene.Trace.Ray(ray, reach).WithoutTags(RoadNetwork.ChunkTag).Run();
if (physics.Hit)
return physics.EndPosition.z;
var meshes = Scene.Trace.Ray(ray, reach).UseRenderMeshes(true, EditorPreferences.BackfaceSelection).WithoutTags("hidden").WithoutTags(RoadNetwork.ChunkTag).UsePhysicsWorld(false).Run();
return meshes.Hit ? meshes.EndPosition.z : _Fallback;
}
public void EndChain()
{
m_LastNodeId = -1;
}
/// <summary>
/// Nodes win over roads, roads over bare ground. A node counts as hit when its on-screen handle is under the
/// cursor or it lies within the snap radius of the ground point.
/// </summary>
private Target ResolveTarget(RoadNetwork _Network, Vector3 _CursorWorld)
{
if (!_Network.IsValid())
return new Target(_CursorWorld, null, null);
var transform = _Network.WorldTransform;
var node = RoadPicker.PickNode(_Network, Gizmo.CurrentRay);
if (node is null && SnapRadius > 0.0f)
node = _Network.FindNearestNode(transform.PointToLocal(_CursorWorld), SnapRadius);
if (node is not null)
return new Target(transform.PointToWorld(node.Position), node, null);
var road = RoadPicker.PickSegment(_Network, Gizmo.CurrentRay);
if (road is { } pick)
return new Target(transform.PointToWorld(pick.LocalPoint), null, pick);
// Over a parking lot: the road meets it on the nearest point of its edge.
if (LotUnder(_Network, transform.PointToLocal(_CursorWorld)) is { } lot)
return new Target(EdgeWorld(_Network, lot, _CursorWorld), null, null, lot);
return new Target(_CursorWorld, null, null);
}
private void PlaceNode(RoadNetworkTool _Root, in Target _Target)
{
var session = SceneEditorSession.Active;
var network = _Root.Network;
bool createNetwork = !network.IsValid();
// Picking up an existing node changes nothing in the graph (starting from it, clicking the node we are
// already on, or joining one we are already connected to): just continue the chain from there.
if (_Target.Node is { } existing && (!IsDrawing || existing.Id == m_LastNodeId || network.FindSegmentBetween(m_LastNodeId, existing.Id) is not null))
{
m_LastNodeId = existing.Id;
return;
}
string undoName = _Target.Road is not null
? (IsDrawing ? "Join Road" : "Branch Road")
: (IsDrawing ? "Extend Road" : "Start Road");
// A brand new network is undone by destroying its GameObject; an existing one by restoring its graph.
var scope = session.UndoScope(undoName);
if (createNetwork)
scope.WithGameObjectCreations();
else
scope.WithComponentChanges(network);
using (scope.Push())
{
if (createNetwork)
network = _Root.CreateNetwork();
RoadNode node;
bool isNew = false;
if (_Target.Node is not null)
{
node = _Target.Node;
}
else if (_Target.Road is { } pick)
{
node = network.SplitSegment(pick.Segment.Id, pick.T, MinEndDistance);
}
else if (_Target.Lot is { } lot)
{
// A new way into the lot, on its edge where the cursor was.
node = network.AddOpening(lot.Id, network.WorldTransform.PointToLocal(_Target.World));
}
else
{
node = network.AddNode(network.WorldTransform.PointToLocal(_Target.World));
isNew = true;
}
if (node is null)
return;
if (IsDrawing && node.Id != m_LastNodeId)
{
// A sharp corner needs both ends of the new road to hold the straight handles the road starts with,
// so the smoothing must not touch either node: the one being extended from (it was placed
// with smoothing on if the toggle changed mid-chain) any more than the new one. Handles the nodes
// already have are left alone, so a road already running through is not kinked.
if (SharpCorners)
{
network.SetAutoTangents(m_LastNodeId, false, false);
network.SetAutoTangents(node.Id, false, false);
}
network.AddSegment(m_LastNodeId, node.Id, Width, Profile, Lanes, OneWay ? Lanes : RoadSegment.DefaultForwardLanes(Lanes), Bridge, Props);
}
// A corner node is a node whose handles all point straight at its neighbours.
if (isNew && SharpCorners)
network.SetAutoTangents(node.Id, false);
m_LastNodeId = node.Id;
}
}
/// <summary>
/// Where on a lot's edge a point over it belongs, in world space.
/// </summary>
private static Vector3 EdgeWorld(RoadNetwork _Network, RoadLot _Lot, Vector3 _World)
{
var transform = _Network.WorldTransform;
int edge = _Lot.NearestEdge(transform.PointToLocal(_World), out float along);
return transform.PointToWorld(_Lot.EdgePoint(edge, along));
}
/// <summary>
/// The parking lot a point in the network's space lies on, or just outside: a road drawn there opens into it.
/// </summary>
private static RoadLot LotUnder(RoadNetwork _Network, Vector3 _Local)
{
foreach (var lot in _Network.Lots)
{
Vector2 local = lot.Local(_Local);
if (MathF.Abs(local.x) <= lot.HalfWidth && MathF.Abs(local.y) <= lot.HalfDepth)
return lot;
}
return null;
}
private bool TryGetCursorPosition(RoadNetwork _Network, out Vector3 _WorldPosition)
{
// The road meshes are skipped so a node placed beside a road lands on the ground, not on the road's edge.
var physics = Trace.WithoutTags(RoadNetwork.ChunkTag).Run();
if (physics.Hit)
{
_WorldPosition = physics.EndPosition;
return true;
}
var meshes = MeshTrace.WithoutTags(RoadNetwork.ChunkTag).Run();
if (meshes.Hit)
{
_WorldPosition = meshes.EndPosition;
return true;
}
// Nothing under the cursor (empty scene, or aiming past the terrain): fall back to a ground plane through
// the node being extended so the road can still be drawn.
float height = 0.0f;
if (IsDrawing && _Network.IsValid() && _Network.FindNode(m_LastNodeId) is { } last)
height = _Network.WorldTransform.PointToWorld(last.Position).z;
var plane = new Plane(new Vector3(0.0f, 0.0f, height), Vector3.Up);
var hit = plane.Trace(Gizmo.CurrentRay, true);
_WorldPosition = hit ?? default;
return hit.HasValue;
}
private void DrawPreview(RoadNetwork _Network, in Target _Target)
{
using (Gizmo.Scope("roadtool2_draw"))
{
Gizmo.Draw.IgnoreDepth = true;
Gizmo.Draw.LineThickness = 2.0f;
if (_Target.Node is not null)
{
Gizmo.Draw.Color = Color.Green;
RoadGizmos.DrawNode(_Target.World, true);
}
else if (_Target.Road is { } pick)
{
// Hovering a road: show the road and where the new node would split it.
Gizmo.Draw.Color = Color.Orange.WithAlpha(0.6f);
if (_Network.TryGetCurve(pick.Segment, out var roadCurve))
RoadGizmos.DrawRoadOutline(roadCurve, _Network.WidthOf(pick.Segment), _Network.WorldTransform);
Gizmo.Draw.Color = Color.Orange;
RoadGizmos.DrawNode(_Target.World, true);
}
else
{
Gizmo.Draw.Color = Color.White;
RoadGizmos.DrawNode(_Target.World, false);
}
if (!IsDrawing || !_Network.IsValid())
return;
if (_Target.Node is not null && _Target.Node.Id == m_LastNodeId)
return;
var transform = _Network.WorldTransform;
Vector3 targetLocal = _Target.Node?.Position ?? _Target.Road?.LocalPoint ?? transform.PointToLocal(_Target.World);
var curve = _Network.PreviewCurve(m_LastNodeId, targetLocal, _Target.Node?.Id ?? -1, SharpCorners);
Gizmo.Draw.Color = Color.White.WithAlpha(0.9f);
RoadGizmos.DrawRoadOutline(curve, Width, transform);
Gizmo.Draw.Color = Color.White.WithAlpha(0.4f);
RoadGizmos.DrawCurve(curve, transform);
if (m_SnappedAngle is { } angle && _Network.FindNode(m_LastNodeId) is { } last)
{
// The snapped direction, drawn out past the target, with the angle it landed on.
Vector3 start = transform.PointToWorld(last.Position);
var direction = new Vector3(MathF.Cos(angle.DegreeToRadian()), MathF.Sin(angle.DegreeToRadian()), 0.0f);
Gizmo.Draw.LineThickness = 1.0f;
Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.5f);
Gizmo.Draw.Line(start, start + direction * GuideLength);
Gizmo.Draw.Color = Color.Cyan;
Gizmo.Draw.ScreenText($"{angle:0}°", _Target.World, new Vector2(12.0f, -12.0f), size: 12.0f);
}
if (m_AlignAnchor is { } anchor)
{
// The node the end is lined up with, and the line between them.
Gizmo.Draw.LineThickness = 2.0f;
Gizmo.Draw.Color = Color.Orange;
DrawDashed(anchor, _Target.World);
RoadGizmos.DrawNode(anchor, true);
}
}
}
private static void DrawDashed(Vector3 _From, Vector3 _To)
{
float length = Vector3.DistanceBetween(_From, _To);
if (length < 0.01f)
return;
// Dashes keep a constant on-screen size.
float dash = MathF.Max(10.0f, Gizmo.Camera.Position.Distance(_To) * 0.02f);
Vector3 direction = (_To - _From) / length;
for (float at = 0.0f; at < length; at += dash * 2.0f)
{
float end = MathF.Min(at + dash, length);
Gizmo.Draw.Line(_From + direction * at, _From + direction * end);
}
}
}
file class RoadDrawToolWidget : ToolSidebarWidget
{
private readonly RoadDrawTool m_Tool;
public RoadDrawToolWidget(RoadDrawTool _Tool)
{
m_Tool = _Tool;
AddTitle("Draw Road", "timeline");
var serialized = _Tool.GetSerialized();
var group = AddGroup("Road");
SidebarRows.AddWide(group, serialized.GetProperty(nameof(RoadDrawTool.Profile)));
SidebarRows.AddWide(group, serialized.GetProperty(nameof(RoadDrawTool.Bridge)));
SidebarRows.AddWide(group, serialized.GetProperty(nameof(RoadDrawTool.Props)));
SidebarRows.Add(group, serialized.GetProperty(nameof(RoadDrawTool.Width)));
SidebarRows.Add(group, serialized.GetProperty(nameof(RoadDrawTool.Lanes)));
SidebarRows.Add(group, serialized.GetProperty(nameof(RoadDrawTool.OneWay)));
SidebarRows.Add(group, serialized.GetProperty(nameof(RoadDrawTool.SnapRadius)));
SidebarRows.Add(group, serialized.GetProperty(nameof(RoadDrawTool.SharpCorners)));
var snapping = AddGroup("Snapping");
SidebarRows.Add(snapping, serialized.GetProperty(nameof(RoadDrawTool.AngleSnap)));
SidebarRows.Add(snapping, serialized.GetProperty(nameof(RoadDrawTool.AngleStep)));
AddShortcuts(
("Place Node", "LMB"),
("Continue From / Join Node", "LMB on node"),
("Branch From / Merge Into Road", "LMB on road"),
("Snap Angle + Align With Nodes", "Hold SHIFT"),
("End Road", "RMB / ESC")
);
Layout.AddStretchCell();
}
[Shortcut("roadtool2.draw-end", "ESC", typeof(SceneViewWidget))]
private void EndChain()
{
m_Tool.EndChain();
}
}