forked from bit/DotRecastNetSim
add log view
This commit is contained in:
parent
c819ded81b
commit
52d7f63ebd
|
@ -19,12 +19,11 @@ freely, subject to the following restrictions:
|
|||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DotRecast.Core;
|
||||
using DotRecast.Detour;
|
||||
using DotRecast.Recast.Demo.Builder;
|
||||
using DotRecast.Recast.Demo.Geom;
|
||||
using DotRecast.Recast.Demo.Settings;
|
||||
using DotRecast.Recast.Demo.UI;
|
||||
|
||||
namespace DotRecast.Recast.Demo.Draw;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ public static class Program
|
|||
Directory.SetCurrentDirectory(workingDirectory);
|
||||
}
|
||||
|
||||
var format = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj} [{MemberName}()] [{ThreadName}:{ThreadId}] at {FilePath}:{LineNumber} {NewLine}{Exception}";
|
||||
var format = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj} [{ThreadName}:{ThreadId}]{NewLine}{Exception}";
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.Enrich.WithThreadId()
|
||||
|
|
|
@ -38,7 +38,7 @@ using DotRecast.Detour.Io;
|
|||
using DotRecast.Recast.Demo.Builder;
|
||||
using DotRecast.Recast.Demo.Draw;
|
||||
using DotRecast.Recast.Demo.Geom;
|
||||
using DotRecast.Recast.Demo.Settings;
|
||||
|
||||
using DotRecast.Recast.Demo.Tools;
|
||||
using DotRecast.Recast.Demo.UI;
|
||||
using static DotRecast.Core.RecastMath;
|
||||
|
@ -50,7 +50,7 @@ public class RecastDemo
|
|||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<RecastDemo>();
|
||||
|
||||
private RcViewSystem _viewSys;
|
||||
private RcCanvas _canvas;
|
||||
private IWindow window;
|
||||
private IInputContext _input;
|
||||
private ImGuiController _imgui;
|
||||
|
@ -107,8 +107,11 @@ public class RecastDemo
|
|||
private int[] viewport;
|
||||
private bool markerPositionSet;
|
||||
private Vector3f markerPosition = new Vector3f();
|
||||
|
||||
private ToolsView toolsUI;
|
||||
private RcSettingsView settingsUI;
|
||||
private RcLogView logUI;
|
||||
|
||||
private long prevFrameTime;
|
||||
private RecastDebugDraw dd;
|
||||
|
||||
|
@ -252,9 +255,6 @@ public class RecastDemo
|
|||
private IWindow CreateWindow()
|
||||
{
|
||||
var monitor = Window.Platforms.First().GetMainMonitor();
|
||||
// // if (monitors.limit() > 1) {
|
||||
// // monitor = monitors[1];
|
||||
// // }
|
||||
var resolution = monitor.VideoMode.Resolution.Value;
|
||||
|
||||
float aspect = 16.0f / 9.0f;
|
||||
|
@ -358,7 +358,6 @@ public class RecastDemo
|
|||
mice.MouseMove += OnMouseMoved;
|
||||
}
|
||||
|
||||
|
||||
_gl = window.CreateOpenGL();
|
||||
|
||||
dd = new RecastDebugDraw(_gl);
|
||||
|
@ -368,27 +367,6 @@ public class RecastDemo
|
|||
|
||||
_imgui = new ImGuiController(_gl, window, _input);
|
||||
|
||||
|
||||
// // if (capabilities.OpenGL43) {
|
||||
// // GL43.glDebugMessageControl(GL43.GL_DEBUG_SOURCE_API, GL43.GL_DEBUG_TYPE_OTHER,
|
||||
// // GL43.GL_DEBUG_SEVERITY_NOTIFICATION,
|
||||
// // (int[]) null, false);
|
||||
// // } else if (capabilities.GL_ARB_debug_output) {
|
||||
// // ARBDebugOutput.glDebugMessageControlARB(ARBDebugOutput.GL_DEBUG_SOURCE_API_ARB,
|
||||
// // ARBDebugOutput.GL_DEBUG_TYPE_OTHER_ARB, ARBDebugOutput.GL_DEBUG_SEVERITY_LOW_ARB, (int[]) null, false);
|
||||
// // }
|
||||
var vendor = _gl.GetStringS(GLEnum.Vendor);
|
||||
Logger.Debug(vendor);
|
||||
|
||||
var version = _gl.GetStringS(GLEnum.Version);
|
||||
Logger.Debug(version);
|
||||
|
||||
var renderGl = _gl.GetStringS(GLEnum.Renderer);
|
||||
Logger.Debug(renderGl);
|
||||
|
||||
var glslString = _gl.GetStringS(GLEnum.ShadingLanguageVersion);
|
||||
Logger.Debug(glslString);
|
||||
|
||||
settingsUI = new RcSettingsView();
|
||||
toolsUI = new ToolsView(
|
||||
new TestNavmeshTool(),
|
||||
|
@ -398,8 +376,20 @@ public class RecastDemo
|
|||
new JumpLinkBuilderTool(),
|
||||
new DynamicUpdateTool()
|
||||
);
|
||||
logUI = new RcLogView();
|
||||
|
||||
_canvas = new RcCanvas(window, settingsUI, toolsUI, logUI);
|
||||
|
||||
var vendor = _gl.GetStringS(GLEnum.Vendor);
|
||||
var version = _gl.GetStringS(GLEnum.Version);
|
||||
var renderGl = _gl.GetStringS(GLEnum.Renderer);
|
||||
var glslString = _gl.GetStringS(GLEnum.ShadingLanguageVersion);
|
||||
|
||||
Logger.Debug(vendor);
|
||||
Logger.Debug(version);
|
||||
Logger.Debug(renderGl);
|
||||
Logger.Debug(glslString);
|
||||
|
||||
_viewSys = new RcViewSystem(window, _input, settingsUI, toolsUI);
|
||||
|
||||
DemoInputGeomProvider geom = loadInputMesh(Loader.ToBytes("nav_test.obj"));
|
||||
sample = new Sample(geom, ImmutableArray<RecastBuilderResult>.Empty, null, settingsUI, dd);
|
||||
|
@ -538,6 +528,8 @@ public class RecastDemo
|
|||
int m_tileSize = settingsUI.getTileSize();
|
||||
long t = FrequencyWatch.Ticks;
|
||||
|
||||
Logger.Information($"build");
|
||||
|
||||
Tuple<IList<RecastBuilderResult>, NavMesh> buildResult;
|
||||
if (settingsUI.isTiled())
|
||||
{
|
||||
|
@ -559,8 +551,22 @@ public class RecastDemo
|
|||
sample.update(sample.getInputGeom(), buildResult.Item1, buildResult.Item2);
|
||||
sample.setChanged(false);
|
||||
settingsUI.setBuildTime((FrequencyWatch.Ticks - t) / TimeSpan.TicksPerMillisecond);
|
||||
settingsUI.setBuildTelemetry(buildResult.Item1.Select(x => x.getTelemetry()).ToList());
|
||||
//settingsUI.setBuildTelemetry(buildResult.Item1.Select(x => x.getTelemetry()).ToList());
|
||||
toolsUI.setSample(sample);
|
||||
|
||||
Logger.Information($"build times");
|
||||
Logger.Information($"-----------------------------------------");
|
||||
var telemetries = buildResult.Item1
|
||||
.Select(x => x.getTelemetry())
|
||||
.SelectMany(x => x.ToList())
|
||||
.GroupBy(x => x.Item1)
|
||||
.ToImmutableSortedDictionary(x => x.Key, x => x.Sum(y => y.Item2));
|
||||
|
||||
foreach (var (key, millis) in telemetries)
|
||||
{
|
||||
Logger.Information($"{key}: {millis} ms");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -706,7 +712,7 @@ public class RecastDemo
|
|||
io.DisplayFramebufferScale = Vector2.One;
|
||||
io.DeltaTime = (float)dt;
|
||||
|
||||
//window.DoEvents();
|
||||
_canvas.Update(dt);
|
||||
_imgui.Update((float)dt);
|
||||
}
|
||||
|
||||
|
@ -736,8 +742,8 @@ public class RecastDemo
|
|||
|
||||
dd.fog(false);
|
||||
|
||||
_viewSys.Draw();
|
||||
_mouseOverMenu = _viewSys.IsMouseOverUI();
|
||||
_canvas.Draw(dt);
|
||||
_mouseOverMenu = _canvas.IsMouseOverUI();
|
||||
_imgui.Render();
|
||||
|
||||
window.SwapBuffers();
|
||||
|
|
|
@ -22,7 +22,8 @@ using System.Collections.Generic;
|
|||
using DotRecast.Detour;
|
||||
using DotRecast.Recast.Demo.Draw;
|
||||
using DotRecast.Recast.Demo.Geom;
|
||||
using DotRecast.Recast.Demo.Settings;
|
||||
|
||||
using DotRecast.Recast.Demo.UI;
|
||||
|
||||
namespace DotRecast.Recast.Demo;
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace DotRecast.Recast.Demo.Tools;
|
||||
|
||||
public class ConsoleTextWriterHook : TextWriter
|
||||
{
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
private readonly Action<string> _event;
|
||||
|
||||
public ConsoleTextWriterHook(Action<string> relay)
|
||||
{
|
||||
_event = relay;
|
||||
}
|
||||
|
||||
public override void Write(char[] buffer, int index, int count)
|
||||
{
|
||||
var s = new string(new Span<char>(buffer, index, count));
|
||||
_event?.Invoke(s);
|
||||
}
|
||||
}
|
|
@ -16,12 +16,12 @@ freely, subject to the following restrictions:
|
|||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace DotRecast.Recast.Demo.UI;
|
||||
|
||||
public interface IRcView
|
||||
{
|
||||
void Bind(RcCanvas canvas);
|
||||
bool IsMouseInside();
|
||||
void Draw();
|
||||
void Update(double dt);
|
||||
void Draw(double dt);
|
||||
}
|
|
@ -16,29 +16,40 @@ freely, subject to the following restrictions:
|
|||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
using System.Numerics;
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.Maths;
|
||||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace DotRecast.Recast.Demo.UI;
|
||||
|
||||
public class RcViewSystem
|
||||
public class RcCanvas
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<RecastDemo>();
|
||||
|
||||
private readonly IWindow _window;
|
||||
private readonly IRcView[] _views;
|
||||
private bool _mouseOverUI;
|
||||
public bool IsMouseOverUI() => _mouseOverUI;
|
||||
|
||||
public RcViewSystem(IWindow window, IInputContext input, params IRcView[] views)
|
||||
public Vector2D<int> Size => _window.Size;
|
||||
|
||||
public RcCanvas(IWindow window, params IRcView[] views)
|
||||
{
|
||||
_window = window;
|
||||
_views = views;
|
||||
foreach (var view in _views)
|
||||
{
|
||||
view.Bind(this);
|
||||
}
|
||||
|
||||
// setupClipboard(window);
|
||||
// glfwSetCharCallback(window, (w, codepoint) => nk_input_unicode(ctx, codepoint));
|
||||
// glContext = new NuklearGL(this);
|
||||
_views = views;
|
||||
}
|
||||
|
||||
|
||||
|
@ -86,13 +97,21 @@ public class RcViewSystem
|
|||
// nk_input_end(ctx);
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
public void Update(double dt)
|
||||
{
|
||||
foreach (var view in _views)
|
||||
{
|
||||
view.Update(dt);
|
||||
}
|
||||
}
|
||||
|
||||
public void Draw(double dt)
|
||||
{
|
||||
_mouseOverUI = false;
|
||||
foreach (IRcView m in _views)
|
||||
foreach (var view in _views)
|
||||
{
|
||||
m.Draw();
|
||||
_mouseOverUI |= m.IsMouseInside();
|
||||
view.Draw(dt);
|
||||
_mouseOverUI |= view.IsMouseInside();
|
||||
// if (_mouseOverUI)
|
||||
// {
|
||||
// Logger.Information("mouse hover!");
|
|
@ -0,0 +1,130 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using DotRecast.Recast.Demo.Tools;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace DotRecast.Recast.Demo.UI;
|
||||
|
||||
public class RcLogView : IRcView
|
||||
{
|
||||
private RcCanvas _canvas;
|
||||
private bool _mouseInside;
|
||||
|
||||
private List<string> _lines = new();
|
||||
private readonly ConcurrentQueue<string> _output = new();
|
||||
private readonly StringBuilder _outputStringBuilder = new();
|
||||
|
||||
private readonly ConcurrentQueue<string> _error = new();
|
||||
private readonly StringBuilder _errorStringBuilder = new();
|
||||
|
||||
|
||||
|
||||
public RcLogView()
|
||||
{
|
||||
Console.SetOut(new ConsoleTextWriterHook(OnOut));
|
||||
Console.SetError(new ConsoleTextWriterHook(OnError));
|
||||
}
|
||||
|
||||
private void OnOut(string log)
|
||||
{
|
||||
_output.Enqueue(log);
|
||||
}
|
||||
|
||||
private void OnError(string log)
|
||||
{
|
||||
_error.Enqueue(log);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_lines.Clear();
|
||||
}
|
||||
|
||||
private void MergeLines(ConcurrentQueue<string> queue, StringBuilder builder)
|
||||
{
|
||||
while (queue.TryDequeue(out var s))
|
||||
{
|
||||
if (s != "\r\n")
|
||||
{
|
||||
builder.Append(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lines.Add(builder.ToString());
|
||||
builder.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Bind(RcCanvas canvas)
|
||||
{
|
||||
_canvas = canvas;
|
||||
}
|
||||
|
||||
public void Update(double dt)
|
||||
{
|
||||
MergeLines(_output, _outputStringBuilder);
|
||||
MergeLines(_error, _errorStringBuilder);
|
||||
|
||||
// buffer
|
||||
if (10240 < _lines.Count)
|
||||
{
|
||||
_lines.RemoveRange(0, _lines.Count - 8196);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMouseInside() => _mouseInside;
|
||||
|
||||
public void Draw(double dt)
|
||||
{
|
||||
int otherWidth = 310;
|
||||
int height = 234;
|
||||
var width = _canvas.Size.X - (otherWidth * 2);
|
||||
//var posX = _canvas.Size.X - width;
|
||||
ImGui.SetNextWindowPos(new Vector2(otherWidth, _canvas.Size.Y - height));
|
||||
ImGui.SetNextWindowSize(new Vector2(width, height));
|
||||
if (!ImGui.Begin("Log", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize))
|
||||
{
|
||||
ImGui.End();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (ImGui.BeginChild("scrolling", Vector2.Zero, false, ImGuiWindowFlags.HorizontalScrollbar))
|
||||
{
|
||||
_mouseInside = ImGui.IsWindowHovered();
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
unsafe
|
||||
{
|
||||
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
|
||||
clipper.Begin(_lines.Count);
|
||||
while (clipper.Step())
|
||||
{
|
||||
for (int lineNo = clipper.DisplayStart; lineNo < clipper.DisplayEnd; lineNo++)
|
||||
{
|
||||
ImGui.TextUnformatted(_lines[lineNo]);
|
||||
}
|
||||
}
|
||||
|
||||
clipper.End();
|
||||
clipper.Destroy();
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
if (ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
|
||||
{
|
||||
ImGui.SetScrollHereY(1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndChild();
|
||||
ImGui.End();
|
||||
}
|
||||
}
|
|
@ -20,13 +20,14 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using DotRecast.Core;
|
||||
using DotRecast.Recast.Demo.Draw;
|
||||
using DotRecast.Recast.Demo.UI;
|
||||
using ImGuiNET;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace DotRecast.Recast.Demo.Settings;
|
||||
namespace DotRecast.Recast.Demo.UI;
|
||||
|
||||
public class RcSettingsView : IRcView
|
||||
{
|
||||
|
@ -63,7 +64,6 @@ public class RcSettingsView : IRcView
|
|||
// public readonly NkColor transparent = NkColor.create();
|
||||
private bool buildTriggered;
|
||||
private long buildTime;
|
||||
private Dictionary<string, long> telemetries = new();
|
||||
private readonly int[] voxels = new int[2];
|
||||
private readonly int[] tiles = new int[2];
|
||||
private int maxTiles;
|
||||
|
@ -77,9 +77,27 @@ public class RcSettingsView : IRcView
|
|||
|
||||
private bool _mouseInside;
|
||||
public bool IsMouseInside() => _mouseInside;
|
||||
public void Draw()
|
||||
|
||||
private RcCanvas _canvas;
|
||||
|
||||
public void Bind(RcCanvas canvas)
|
||||
{
|
||||
ImGui.Begin("Properties");
|
||||
_canvas = canvas;
|
||||
}
|
||||
|
||||
public void Update(double dt)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Draw(double dt)
|
||||
{
|
||||
int width = 310;
|
||||
var posX = _canvas.Size.X - width;
|
||||
ImGui.SetNextWindowPos(new Vector2(posX, 0));
|
||||
ImGui.SetNextWindowSize(new Vector2(width, _canvas.Size.Y));
|
||||
ImGui.Begin("Properties", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize);
|
||||
|
||||
_mouseInside = ImGui.IsWindowHovered();
|
||||
|
||||
ImGui.Text("Input Mesh");
|
||||
|
@ -101,6 +119,7 @@ public class RcSettingsView : IRcView
|
|||
meshInputFilePath = picker.SelectedFile;
|
||||
ImFilePicker.RemoveFilePicker(strLoadSourceGeom);
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
else
|
||||
|
@ -176,13 +195,11 @@ public class RcSettingsView : IRcView
|
|||
ImGui.Text($"Max Tiles {maxTiles}");
|
||||
ImGui.Text($"Max Polys {maxPolys}");
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
|
||||
ImGui.Text($"Build Time: {buildTime} ms");
|
||||
foreach (var (key, millis) in telemetries)
|
||||
{
|
||||
ImGui.Text($"{key}: {millis} ms");
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
buildTriggered = ImGui.Button("Build");
|
||||
const string strLoadNavMesh = "Load Nav Mesh...";
|
||||
|
@ -200,6 +217,7 @@ public class RcSettingsView : IRcView
|
|||
Console.WriteLine(picker.SelectedFile);
|
||||
ImFilePicker.RemoveFilePicker(strLoadNavMesh);
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
|
@ -208,10 +226,7 @@ public class RcSettingsView : IRcView
|
|||
ImGui.Text("Draw");
|
||||
ImGui.Separator();
|
||||
|
||||
DrawMode.Values.forEach(dm =>
|
||||
{
|
||||
ImGui.RadioButton(dm.Text, ref drawMode, dm.Idx);
|
||||
});
|
||||
DrawMode.Values.forEach(dm => { ImGui.RadioButton(dm.Text, ref drawMode, dm.Idx); });
|
||||
ImGui.NewLine();
|
||||
|
||||
ImGui.End();
|
||||
|
@ -287,15 +302,6 @@ public class RcSettingsView : IRcView
|
|||
this.buildTime = buildTime;
|
||||
}
|
||||
|
||||
public void setBuildTelemetry(IList<Telemetry> telemetries)
|
||||
{
|
||||
this.telemetries = telemetries
|
||||
.SelectMany(x => x.ToList())
|
||||
.GroupBy(x => x.Item1)
|
||||
.ToDictionary(x => x.Key, x => x.Sum(y => y.Item2));
|
||||
|
||||
}
|
||||
|
||||
public DrawMode getDrawMode()
|
||||
{
|
||||
return DrawMode.Values[drawMode];
|
|
@ -17,12 +17,13 @@ freely, subject to the following restrictions:
|
|||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
using System.Numerics;
|
||||
using DotRecast.Core;
|
||||
using DotRecast.Recast.Demo.Tools;
|
||||
using DotRecast.Recast.Demo.UI;
|
||||
using ImGuiNET;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
namespace DotRecast.Recast.Demo.Tools;
|
||||
namespace DotRecast.Recast.Demo.UI;
|
||||
|
||||
public class ToolsView : IRcView
|
||||
{
|
||||
|
@ -40,9 +41,24 @@ public class ToolsView : IRcView
|
|||
private bool _mouseInside;
|
||||
public bool IsMouseInside() => _mouseInside;
|
||||
|
||||
public void Draw()
|
||||
private RcCanvas _canvas;
|
||||
|
||||
public void Bind(RcCanvas canvas)
|
||||
{
|
||||
ImGui.Begin("Tools");
|
||||
_canvas = canvas;
|
||||
}
|
||||
|
||||
public void Update(double dt)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Draw(double dt)
|
||||
{
|
||||
int width = 310;
|
||||
ImGui.SetNextWindowPos(new Vector2(0, 0));
|
||||
ImGui.SetNextWindowSize(new Vector2(width, _canvas.Size.Y));
|
||||
ImGui.Begin("Tools", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize);
|
||||
_mouseInside = ImGui.IsWindowHovered();
|
||||
|
||||
for (int i = 0; i < tools.Length; ++i)
|
||||
|
@ -50,6 +66,7 @@ public class ToolsView : IRcView
|
|||
var tool = tools[i];
|
||||
ImGui.RadioButton(tool.getName(), ref _currentToolIdx, i);
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
|
||||
if (0 > _currentToolIdx || _currentToolIdx >= tools.Length)
|
Loading…
Reference in New Issue