Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Utils/ToolBox.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Diagnostics.CodeAnalysis;
8 using Barotrauma.IO;
9 using System.Linq;
10 using System.Net;
11 using System.Reflection;
12 using System.Security.Cryptography;
13 using System.Text;
14 
15 namespace Barotrauma
16 {
17  [Obsolete("Use named tuples instead.")]
18  public class Pair<T1, T2>
19  {
20  public T1 First { get; set; }
21  public T2 Second { get; set; }
22 
23  public Pair(T1 first, T2 second)
24  {
25  First = first;
26  Second = second;
27  }
28  }
29 
30  internal readonly record struct SquareLine(Vector2[] Points, SquareLine.LineType Type)
31  {
32  internal enum LineType
33  {
42  FourPointForwardsLine,
51  SixPointBackwardsLine
52  }
53  }
54 
55  static partial class ToolBox
56  {
57  public static bool IsProperFilenameCase(string filename)
58  {
59  //File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds.
60  //It also seems Path.GetFullPath may return a path with an incorrect case on Windows when the case of any of the game's
61  //parent folders have been changed.
62 #if !DEBUG && !LINUX
63  return true;
64 #endif
65 
66  CorrectFilenameCase(filename, out bool corrected);
67 
68  return !corrected;
69  }
70 
71  public static string CorrectFilenameCase(string filename, out bool corrected, string directory = "")
72  {
73  char[] delimiters = { '/', '\\' };
74  string[] subDirs = filename.Split(delimiters);
75  string originalFilename = filename;
76  filename = "";
77  corrected = false;
78 
79 #if !WINDOWS
80  if (File.Exists(originalFilename))
81  {
82  return originalFilename;
83  }
84 #endif
85 
86  string startPath = directory ?? "";
87 
88  string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/');
89  if (originalFilename.Replace('\\', '/').StartsWith(saveFolder))
90  {
91  //paths that lead to the save folder might have incorrect case,
92  //mainly if they come from a filelist
93  startPath = saveFolder.EndsWith('/') ? saveFolder : $"{saveFolder}/";
94  filename = startPath;
95  subDirs = subDirs.Skip(saveFolder.Split('/').Length).ToArray();
96  }
97  else if (Path.IsPathRooted(originalFilename))
98  {
99  #warning TODO: incorrect assumption or...? Figure out what this was actually supposed to fix, if anything. Might've been a perf thing.
100  return originalFilename; //assume that rooted paths have correct case since these are generated by the game
101  }
102 
103  for (int i = 0; i < subDirs.Length; i++)
104  {
105  if (i == subDirs.Length - 1 && string.IsNullOrEmpty(subDirs[i]))
106  {
107  break;
108  }
109 
110  string subDir = subDirs[i].TrimEnd();
111  string enumPath = Path.Combine(startPath, filename);
112 
113  if (string.IsNullOrWhiteSpace(filename))
114  {
115  enumPath = string.IsNullOrWhiteSpace(startPath) ? "./" : startPath;
116  }
117 
118  string[] filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToArray();
119 
120  if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal)))
121  {
122  filename += subDir;
123  }
124  else
125  {
126  string[] correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToArray();
127  if (correctedPaths.Any())
128  {
129  corrected = true;
130  filename += correctedPaths.First();
131  }
132  else
133  {
134  //DebugConsole.ThrowError($"File \"{originalFilename}\" not found!");
135  corrected = false;
136  return originalFilename;
137  }
138  }
139  if (i < subDirs.Length - 1) { filename += "/"; }
140  }
141 
142  return filename;
143  }
144 
145  public static string RemoveInvalidFileNameChars(string fileName)
146  {
147  var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform().Concat(new char[] {';'});
148  foreach (char invalidChar in invalidChars)
149  {
150  fileName = fileName.Replace(invalidChar.ToString(), "");
151  }
152  return fileName;
153  }
154 
155  private static readonly System.Text.RegularExpressions.Regex removeBBCodeRegex =
156  new System.Text.RegularExpressions.Regex(@"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]");
157 
158  public static string RemoveBBCodeTags(string str)
159  {
160  if (string.IsNullOrEmpty(str)) { return str; }
161  return removeBBCodeRegex.Replace(str, "");
162  }
163 
164  public static string RandomSeed(int length)
165  {
166  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
167  return new string(
168  Enumerable.Repeat(chars, length)
169  .Select(s => s[Rand.Int(s.Length)])
170  .ToArray());
171  }
172 
173  public static int IdentifierToInt(Identifier id) => StringToInt(id.Value.ToLowerInvariant());
174 
175  public static int StringToInt(string str)
176  {
177  str = str.Substring(0, Math.Min(str.Length, 32));
178 
179  str = str.PadLeft(4, 'a');
180 
181  byte[] asciiBytes = Encoding.ASCII.GetBytes(str);
182 
183  for (int i = 4; i < asciiBytes.Length; i++)
184  {
185  asciiBytes[i % 4] ^= asciiBytes[i];
186  }
187 
188  return BitConverter.ToInt32(asciiBytes, 0);
189  }
190 
194  public static string ConvertInputType(string inputType)
195  {
196  if (inputType == "ActionHit" || inputType == "Action") return "Use";
197  if (inputType == "SecondaryHit" || inputType == "Secondary") return "Aim";
198 
199  return inputType;
200  }
201 
208  public static string GetDebugSymbol(bool isFinished, bool isRunning = false)
209  {
210  return isRunning ? "[‖color:243,162,50‖x‖color:end‖]" : $"[‖color:{(isFinished ? "0,255,0‖x" : "255,0,0‖o")}‖color:end‖]";
211  }
212 
218  public static string ColorizeObject(this object obj)
219  {
220  string color = obj switch
221  {
222  bool b => b ? "80,250,123" : "255,85,85",
223  string _ => "241,250,140",
224  Identifier _ => "241,250,140",
225  int _ => "189,147,249",
226  float _ => "189,147,249",
227  double _ => "189,147,249",
228  null => "255,85,85",
229  _ => "139,233,253"
230  };
231 
232  return obj is string || obj is Identifier
233  ? $"‖color:{color}‖\"{obj}\"‖color:end‖"
234  : $"‖color:{color}‖{obj ?? "null"}‖color:end‖";
235  }
236 
237  // Convert an RGB value into an HLS value.
238  public static Vector3 RgbToHLS(Vector3 color)
239  {
240  double h, l, s;
241 
242  double double_r = color.X;
243  double double_g = color.Y;
244  double double_b = color.Z;
245 
246  // Get the maximum and minimum RGB components.
247  double max = double_r;
248  if (max < double_g) max = double_g;
249  if (max < double_b) max = double_b;
250 
251  double min = double_r;
252  if (min > double_g) min = double_g;
253  if (min > double_b) min = double_b;
254 
255  double diff = max - min;
256  l = (max + min) / 2;
257  if (Math.Abs(diff) < 0.00001)
258  {
259  s = 0;
260  h = 0; // H is really undefined.
261  }
262  else
263  {
264  if (l <= 0.5) s = diff / (max + min);
265  else s = diff / (2 - max - min);
266 
267  double r_dist = (max - double_r) / diff;
268  double g_dist = (max - double_g) / diff;
269  double b_dist = (max - double_b) / diff;
270 
271  if (double_r == max) h = b_dist - g_dist;
272  else if (double_g == max) h = 2 + r_dist - b_dist;
273  else h = 4 + g_dist - r_dist;
274 
275  h = h * 60;
276  if (h < 0) h += 360;
277  }
278 
279  return new Vector3((float)h, (float)l, (float)s);
280  }
281 
285  public static int LevenshteinDistance(string s, string t)
286  {
287  int n = s.Length;
288  int m = t.Length;
289  int[,] d = new int[n + 1, m + 1];
290 
291  if (n == 0 || m == 0) return 0;
292 
293  for (int i = 0; i <= n; d[i, 0] = i++) ;
294  for (int j = 0; j <= m; d[0, j] = j++) ;
295 
296  for (int i = 1; i <= n; i++)
297  {
298  for (int j = 1; j <= m; j++)
299  {
300  int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
301 
302  d[i, j] = Math.Min(
303  Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
304  d[i - 1, j - 1] + cost);
305  }
306  }
307 
308  return d[n, m];
309  }
310 
311  public static LocalizedString SecondsToReadableTime(float seconds)
312  {
313  int s = (int)(seconds % 60.0f);
314  if (seconds < 60.0f)
315  {
316  return TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString());
317  }
318 
319  int h = (int)(seconds / (60.0f * 60.0f));
320  int m = (int)((seconds / 60.0f) % 60);
321 
322  LocalizedString text = "";
323  if (h != 0) { text = TextManager.GetWithVariable("timeformathours", "[hours]", h.ToString()); }
324  if (m != 0)
325  {
326  LocalizedString minutesText = TextManager.GetWithVariable("timeformatminutes", "[minutes]", m.ToString());
327  text = text.IsNullOrEmpty() ? minutesText : LocalizedString.Join(" ", text, minutesText);
328  }
329  if (s != 0)
330  {
331  LocalizedString secondsText = TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString());
332  text = text.IsNullOrEmpty() ? secondsText : LocalizedString.Join(" ", text, secondsText);
333  }
334  return text;
335  }
336 
337  private static Dictionary<string, List<string>> cachedLines = new Dictionary<string, List<string>>();
338  public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
339  {
340  List<string> lines;
341  if (cachedLines.ContainsKey(filePath))
342  {
343  lines = cachedLines[filePath];
344  }
345  else
346  {
347  try
348  {
349  lines = File.ReadAllLines(filePath).ToList();
350  cachedLines.Add(filePath, lines);
351  if (lines.Count == 0)
352  {
353  DebugConsole.ThrowError("File \"" + filePath + "\" is empty!");
354  return "";
355  }
356  }
357  catch (Exception e)
358  {
359  DebugConsole.ThrowError("Couldn't open file \"" + filePath + "\"!", e);
360  return "";
361  }
362  }
363 
364  if (lines.Count == 0) return "";
365  return lines[Rand.Range(0, lines.Count, randSync)];
366  }
367 
371  public static IReadMessage ExtractBits(this IReadMessage originalBuffer, int numberOfBits)
372  {
373  var buffer = new ReadWriteMessage();
374 
375  for (int i = 0; i < numberOfBits; i++)
376  {
377  bool bit = originalBuffer.ReadBoolean();
378  buffer.WriteBoolean(bit);
379  }
380  buffer.BitPosition = 0;
381 
382  return buffer;
383  }
384 
385  public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Rand.RandSync randSync)
386  {
387  return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync));
388  }
389 
390  public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Random random)
391  {
392  if (typeof(PrefabWithUintIdentifier).IsAssignableFrom(typeof(T)))
393  {
394  objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0);
395  }
396  List<T> objectList = objects.ToList();
397  List<float> weights = objectList.Select(weightMethod).ToList();
398  return SelectWeightedRandom(objectList, weights, random);
399  }
400 
401  public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Rand.RandSync randSync)
402  {
403  return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync));
404  }
405 
406  public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Random random)
407  {
408  if (objects.Count == 0) { return default; }
409 
410  if (objects.Count != weights.Count)
411  {
412  DebugConsole.ThrowError("Error in SelectWeightedRandom, number of objects does not match the number of weights.\n" + Environment.StackTrace.CleanupStackTrace());
413  return objects[0];
414  }
415 
416  float totalWeight = weights.Sum();
417 
418  float randomNum = (float)(random.NextDouble() * totalWeight);
419  for (int i = 0; i < objects.Count; i++)
420  {
421  if (randomNum <= weights[i])
422  {
423  return objects[i];
424  }
425  randomNum -= weights[i];
426  }
427  return default;
428  }
429 
433  public static T CreateCopy<T>(this T source, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) where T : new() => CopyValues(source, new T(), flags);
434  public static T CopyValuesTo<T>(this T source, T target, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) => CopyValues(source, target, flags);
435 
439  public static T CopyValues<T>(T source, T destination, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public)
440  {
441  if (source == null)
442  {
443  throw new Exception("Failed to copy object. Source is null.");
444  }
445  if (destination == null)
446  {
447  throw new Exception("Failed to copy object. Destination is null.");
448  }
449  Type type = source.GetType();
450  var properties = type.GetProperties(flags);
451  foreach (var property in properties)
452  {
453  if (property.CanWrite)
454  {
455  property.SetValue(destination, property.GetValue(source, null), null);
456  }
457  }
458  var fields = type.GetFields(flags);
459  foreach (var field in fields)
460  {
461  field.SetValue(destination, field.GetValue(source));
462  }
463  // Check that the fields match.Uncomment to apply the test, if in doubt.
464  //if (fields.Any(f => { var value = f.GetValue(destination); return value == null || !value.Equals(f.GetValue(source)); }))
465  //{
466  // throw new Exception("Failed to copy some of the fields.");
467  //}
468  return destination;
469  }
470 
471  public static void SiftElement<T>(this List<T> list, int from, int to)
472  {
473  if (from < 0 || from >= list.Count) { throw new ArgumentException($"from parameter out of range (from={from}, range=[0..{list.Count - 1}])"); }
474  if (to < 0 || to >= list.Count) { throw new ArgumentException($"to parameter out of range (to={to}, range=[0..{list.Count - 1}])"); }
475 
476  T elem = list[from];
477  if (from > to)
478  {
479  for (int i = from; i > to; i--)
480  {
481  list[i] = list[i - 1];
482  }
483  list[to] = elem;
484  }
485  else if (from < to)
486  {
487  for (int i = from; i < to; i++)
488  {
489  list[i] = list[i + 1];
490  }
491  list[to] = elem;
492  }
493  }
494 
495  public static string EscapeCharacters(string str)
496  {
497  return str.Replace("\\", "\\\\").Replace("\"", "\\\"");
498  }
499 
500  public static string UnescapeCharacters(string str)
501  {
502  string retVal = "";
503  for (int i = 0; i < str.Length; i++)
504  {
505  if (str[i] != '\\')
506  {
507  retVal += str[i];
508  }
509  else if (i+1<str.Length)
510  {
511  if (str[i+1] == '\\')
512  {
513  retVal += "\\";
514  }
515  else if (str[i+1] == '\"')
516  {
517  retVal += "\"";
518  }
519  i++;
520  }
521  }
522  return retVal;
523  }
524 
525  public static string[] SplitCommand(string command)
526  {
527  command = command.Trim();
528 
529  List<string> commands = new List<string>();
530  int escape = 0;
531  bool inQuotes = false;
532  string piece = "";
533 
534  for (int i = 0; i < command.Length; i++)
535  {
536  if (command[i] == '\\')
537  {
538  if (escape == 0) escape = 2;
539  else piece += '\\';
540  }
541  else if (command[i] == '"')
542  {
543  if (escape == 0) inQuotes = !inQuotes;
544  else piece += '"';
545  }
546  else if (command[i] == ' ' && !inQuotes)
547  {
548  if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece);
549  piece = "";
550  }
551  else if (escape == 0) piece += command[i];
552 
553  if (escape > 0) escape--;
554  }
555 
556  if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece); //add final piece
557 
558  return commands.ToArray();
559  }
560 
572  public static string CleanUpPathCrossPlatform(this string path, bool correctFilenameCase = true, string directory = "")
573  {
574  if (string.IsNullOrEmpty(path)) { return ""; }
575 
576  path = path
577  .Replace('\\', '/');
578  if (path.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
579  {
580  path = path.Substring("file:".Length);
581  }
582  while (path.IndexOf("//") >= 0)
583  {
584  path = path.Replace("//", "/");
585  }
586 
587  if (correctFilenameCase)
588  {
589  string correctedPath = CorrectFilenameCase(path, out _, directory);
590  if (!string.IsNullOrEmpty(correctedPath)) { path = correctedPath; }
591  }
592 
593  return path;
594  }
595 
604  public static string CleanUpPath(this string path)
605  {
606  return path.CleanUpPathCrossPlatform(
607  correctFilenameCase:
608 #if WINDOWS
609  false
610 #else
611  true
612 #endif
613  );
614  }
615 
616  public static float GetEasing(TransitionMode easing, float t)
617  {
618  return easing switch
619  {
620  TransitionMode.Smooth => MathUtils.SmoothStep(t),
621  TransitionMode.Smoother => MathUtils.SmootherStep(t),
622  TransitionMode.EaseIn => MathUtils.EaseIn(t),
623  TransitionMode.EaseOut => MathUtils.EaseOut(t),
624  TransitionMode.Exponential => t * t,
625  TransitionMode.Linear => t,
626  _ => t,
627  };
628  }
629 
630  public static Rectangle GetWorldBounds(Point center, Point size)
631  {
632  Point halfSize = size.Divide(2);
633  Point topLeft = new Point(center.X - halfSize.X, center.Y + halfSize.Y);
634  return new Rectangle(topLeft, size);
635  }
636 
637  public static void ThrowIfNull<T>([NotNull] T o)
638  {
639  if (o is null) { throw new ArgumentNullException(); }
640  }
641 
645  public static string GetFormattedPercentage(float v)
646  {
647  return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value;
648  }
649 
650  private static readonly ImmutableHashSet<char> affectedCharacters = ImmutableHashSet.Create('%', '+', '%');
651 
659  public static string ExtendColorToPercentageSigns(string original)
660  {
661  const string colorEnd = "‖color:end‖",
662  colorStart = "‖color:";
663 
664  const char definitionIndicator = '‖';
665 
666  char[] chars = original.ToCharArray();
667 
668  for (int i = 0; i < chars.Length; i++)
669  {
670  if (!TryGetAt(i, chars, out char currentChar) || !affectedCharacters.Contains(currentChar)) { continue; }
671 
672  // look behind
673  if (TryGetAt(i - 1, chars, out char c) && c is definitionIndicator)
674  {
675  int offset = colorEnd.Length;
676 
677  if (MatchesSequence(i - offset, colorEnd, chars))
678  {
679  // push the color end tag forwards until the character is within the tag
680  char prev = currentChar;
681  for (int k = i - offset; k <= i; k++)
682  {
683  if (!TryGetAt(k, chars, out c)) { continue; }
684 
685  chars[k] = prev;
686  prev = c;
687  }
688  continue;
689  }
690  }
691 
692  // look ahead
693  if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator)
694  {
695  if (!MatchesSequence(i + 1, colorStart, chars)) { continue; }
696 
697  int offset = FindNextDefinitionOffset(i, colorStart.Length, chars);
698 
699  // we probably reached the end of the string
700  if (offset > chars.Length) { continue; }
701 
702  // push the color start tag back until the character is within the tag
703  char prev = currentChar;
704  for (int k = i + offset; k >= i; k--)
705  {
706  if (!TryGetAt(k, chars, out c)) { continue; }
707 
708  chars[k] = prev;
709  prev = c;
710  }
711 
712  // skip needlessly checking this section again since we already know what's ahead
713  i += offset;
714  }
715  }
716 
717  static int FindNextDefinitionOffset(int index, int initialOffset, char[] chars)
718  {
719  int offset = initialOffset;
720  while (TryGetAt(index + offset, chars, out char c) && c is not definitionIndicator) { offset++; }
721  return offset;
722  }
723 
724  static bool MatchesSequence(int index, string sequence, char[] chars)
725  {
726  for (int i = 0; i < sequence.Length; i++)
727  {
728  if (!TryGetAt(index + i, chars, out char c) || c != sequence[i]) { return false; }
729  }
730 
731  return true;
732  }
733 
734  static bool TryGetAt(int i, char[] chars, out char c)
735  {
736  if (i >= 0 && i < chars.Length)
737  {
738  c = chars[i];
739  return true;
740  }
741 
742  c = default;
743  return false;
744  }
745 
746  return new string(chars);
747  }
748 
749  public static bool StatIdentifierMatches(Identifier original, Identifier match)
750  {
751  if (original == match) { return true; }
752  return Matches(original, match) || Matches(match, original);
753 
754  static bool Matches(Identifier a, Identifier b)
755  {
756  for (int i = 0; i < b.Value.Length; i++)
757  {
758  if (i >= a.Value.Length) { return b[i] is '~'; }
759  if (!CharEquals(a[i], b[i])) { return false; }
760  }
761  return false;
762  }
763 
764  static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b);
765  }
766 
767  public static bool EquivalentTo(this IPEndPoint self, IPEndPoint other)
768  => self.Address.EquivalentTo(other.Address) && self.Port == other.Port;
769 
770  public static bool EquivalentTo(this IPAddress self, IPAddress other)
771  {
772  if (self.IsIPv4MappedToIPv6) { self = self.MapToIPv4(); }
773  if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); }
774  return self.Equals(other);
775  }
776 
780  public static float ShortAudioSampleToFloat(short value)
781  {
782  return value / 32767f;
783  }
784 
788  public static short FloatToShortAudioSample(float value)
789  {
790  int temp = (int)(32767 * value);
791  if (temp > short.MaxValue)
792  {
793  temp = short.MaxValue;
794  }
795  else if (temp < short.MinValue)
796  {
797  temp = short.MinValue;
798  }
799  return (short)temp;
800  }
801 
803  public static SquareLine GetSquareLineBetweenPoints(Vector2 start, Vector2 end, float knobLength = 24f)
804  {
805  Vector2[] points = new Vector2[6];
806 
807  // set the start and end points
808  points[0] = points[1] = points[2] = start;
809  points[5] = points[4] = points[3] = end;
810 
811  points[2].X += (points[3].X - points[2].X) / 2;
812  points[2].X = Math.Max(points[2].X, points[0].X + knobLength);
813  points[3].X = points[2].X;
814 
815  bool isBehind = false;
816 
817  // if the node is "behind" us do some magic to make the line curve to prevent overlapping
818  if (points[2].X <= points[0].X + knobLength)
819  {
820  isBehind = true;
821  points[1].X += knobLength;
822  points[2].X = points[2].X;
823  points[2].Y += (points[4].Y - points[1].Y) / 2;
824  }
825 
826  if (points[3].X >= points[5].X - knobLength)
827  {
828  isBehind = true;
829  points[4].X -= knobLength;
830  points[3].X = points[4].X;
831  points[3].Y -= points[3].Y - points[2].Y;
832  }
833 
834  SquareLine.LineType type = isBehind
835  ? SquareLine.LineType.SixPointBackwardsLine
836  : SquareLine.LineType.FourPointForwardsLine;
837 
838  return new SquareLine(points, type);
839  }
840 
848  public static Vector2 GetClosestPointOnRectangle(RectangleF rect, Vector2 point)
849  {
850  Vector2 closest = new Vector2(
851  MathHelper.Clamp(point.X, rect.Left, rect.Right),
852  MathHelper.Clamp(point.Y, rect.Top, rect.Bottom));
853 
854  if (point.X < rect.Left)
855  {
856  closest.X = rect.Left;
857  }
858  else if (point.X > rect.Right)
859  {
860  closest.X = rect.Right;
861  }
862 
863  if (point.Y < rect.Top)
864  {
865  closest.Y = rect.Top;
866  }
867  else if (point.Y > rect.Bottom)
868  {
869  closest.Y = rect.Bottom;
870  }
871 
872  return closest;
873  }
874  }
875 }
TransitionMode
Definition: Enums.cs:6