diff --git a/src/Robots.Grasshopper/ComponentButton.cs b/src/Robots.Grasshopper/ComponentButton.cs new file mode 100644 index 0000000..04858a0 --- /dev/null +++ b/src/Robots.Grasshopper/ComponentButton.cs @@ -0,0 +1,97 @@ +using System.Drawing; +using System.Windows.Forms; +using Grasshopper.Kernel; +using Grasshopper.Kernel.Attributes; +using Grasshopper.GUI; +using Grasshopper.GUI.Canvas; + +namespace Robots.Grasshopper; + +class ComponentButton : GH_ComponentAttributes +{ + const int _buttonSize = 18; + + readonly string _label; + readonly Action _action; + + RectangleF _buttonBounds; + bool _mouseDown; + + public ComponentButton(GH_Component owner, string label, Action action) : base(owner) + { + _label = label; + _action = action; + } + + protected override void Layout() + { + base.Layout(); + + const int margin = 3; + + var bounds = GH_Convert.ToRectangle(Bounds); + var button = bounds; + + button.X += margin; + button.Width -= margin * 2; + button.Y = bounds.Bottom; + button.Height = _buttonSize; + + bounds.Height += _buttonSize + margin; + + Bounds = bounds; + _buttonBounds = button; + } + + protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel) + { + base.Render(canvas, graphics, channel); + + if (channel == GH_CanvasChannel.Objects) + { + var prototype = GH_FontServer.StandardAdjusted; + var font = GH_FontServer.NewFont(prototype, 6f / GH_GraphicsUtil.UiScale); + var radius = 3; + var highlight = !_mouseDown ? 8 : 0; + + using var button = GH_Capsule.CreateTextCapsule(_buttonBounds, _buttonBounds, GH_Palette.Black, _label, font, radius, highlight); + button.Render(graphics, false, Owner.Locked, false); + } + } + + void SetMouseDown(bool value, GH_Canvas canvas, GH_CanvasMouseEvent e) + { + if (Owner.Locked || _mouseDown == value) + return; + + if (value && e.Button != MouseButtons.Left) + return; + + if (!_buttonBounds.Contains(e.CanvasLocation)) + return; + + if (_mouseDown && !value) + _action(); + + _mouseDown = value; + canvas.Invalidate(); + } + + public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e) + { + SetMouseDown(true, sender, e); + return base.RespondToMouseDown(sender, e); + } + + public override GH_ObjectResponse RespondToMouseUp(GH_Canvas sender, GH_CanvasMouseEvent e) + { + SetMouseDown(false, sender, e); + return base.RespondToMouseUp(sender, e); + } + + public override GH_ObjectResponse RespondToMouseMove(GH_Canvas sender, GH_CanvasMouseEvent e) + { + SetMouseDown(false, sender, e); + return base.RespondToMouseMove(sender, e); + } +} \ No newline at end of file diff --git a/src/Robots.Grasshopper/ComponentForm.cs b/src/Robots.Grasshopper/ComponentForm.cs new file mode 100644 index 0000000..523052d --- /dev/null +++ b/src/Robots.Grasshopper/ComponentForm.cs @@ -0,0 +1,46 @@ +using System.ComponentModel; +using Eto.Drawing; +using Eto.Forms; + +namespace Robots.Grasshopper; + +class ComponentForm : Form +{ + public ComponentForm() + { + Maximizable = false; + Minimizable = false; + Resizable = false; + Topmost = true; + ShowInTaskbar = true; + Owner = Rhino.UI.RhinoEtoApp.MainWindow; + } + + public override bool Visible + { + get => base.Visible; + set + { + if (value) + CenterOnMouse(); + + base.Visible = value; + } + } + + void CenterOnMouse() + { + var mousePos = Mouse.Position; + int x = (int)mousePos.X + 20; + int y = (int)mousePos.Y - MinimumSize.Height / 2; + Location = new Point(x, y); + } + + protected override void OnClosing(CancelEventArgs e) + { + Visible = false; + e.Cancel = true; + + base.OnClosing(e); + } +} \ No newline at end of file diff --git a/src/Robots.Grasshopper/Program/Simulation.cs b/src/Robots.Grasshopper/Program/Simulation.cs index 2559a41..ec297af 100644 --- a/src/Robots.Grasshopper/Program/Simulation.cs +++ b/src/Robots.Grasshopper/Program/Simulation.cs @@ -1,26 +1,18 @@ -using System.ComponentModel; -using Grasshopper.Kernel; +using Grasshopper.Kernel; using Grasshopper.Kernel.Types; -using Eto.Drawing; -using Eto.Forms; namespace Robots.Grasshopper; -public sealed class Simulation : GH_Component, IDisposable +public sealed class Simulation : GH_Component { - readonly AnimForm _form; - double _speed = 1; - DateTime _lastTime; + SimulationForm? _form; + DateTime? _lastTime; double _time = 0; - double _sliderTime = 0; + double _lastInputTime = 0; - public Simulation() : base("Program simulation", "Sim", "Rough simulation of the robot program, right click for playback controls", "Robots", "Components") - { - _form = new AnimForm(this) - { - Owner = Rhino.UI.RhinoEtoApp.MainWindow - }; - } + internal double Speed = 1; + + public Simulation() : base("Program simulation", "Sim", "Rough simulation of the robot program, right click for playback controls", "Robots", "Components") { } public override GH_Exposure Exposure => GH_Exposure.quinary; public override Guid ComponentGuid => new("{6CE35140-A625-4686-B8B3-B734D9A36CFC}"); @@ -47,10 +39,11 @@ protected override void RegisterOutputParams(GH_OutputParamManager pManager) protected override void SolveInstance(IGH_DataAccess DA) { GH_Program? program = null; - GH_Number? sliderTimeGH = null; + GH_Number? inputTimeGh = null; GH_Boolean? isNormalized = null; + if (!DA.GetData(0, ref program) || program is null) { return; } - if (!DA.GetData(1, ref sliderTimeGH) || sliderTimeGH is null) { return; } + if (!DA.GetData(1, ref inputTimeGh) || inputTimeGh is null) { return; } if (!DA.GetData(2, ref isNormalized) || isNormalized is null) { return; } if (program?.Value is not Program p) @@ -59,12 +52,27 @@ protected override void SolveInstance(IGH_DataAccess DA) return; } - p.MeshPoser ??= new RhinoMeshPoser(p.RobotSystem); + var inputTime = inputTimeGh.Value; + inputTime = (isNormalized.Value) ? inputTime * p.Duration : inputTime; + + if (_lastInputTime != inputTime) + { + if (_lastTime is not null) + Pause(); + + _time = inputTime; + _lastInputTime = inputTime; + } - _sliderTime = (isNormalized.Value) ? sliderTimeGH.Value * p.Duration : sliderTimeGH.Value; - if (!_form.Visible) _time = _sliderTime; + if (_time < 0 || _time > p.Duration) + { + _time = Rhino.RhinoMath.Clamp(_time, 0, p.Duration); + Pause(); + } + p.MeshPoser ??= new RhinoMeshPoser(p.RobotSystem); p.Animate(_time, false); + var currentPose = p.CurrentSimulationPose; var currentKinematics = currentPose.Kinematics; var currentCellTarget = p.Targets[currentPose.TargetIndex]; @@ -74,7 +82,7 @@ protected override void SolveInstance(IGH_DataAccess DA) var planes = currentKinematics.SelectMany(x => x.Planes).ToList(); var meshes = ((RhinoMeshPoser)p.MeshPoser).Meshes; - if (errors.Count() > 0) + if (errors.Any()) AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Errors in solution"); DA.SetDataList(0, meshes); @@ -85,143 +93,70 @@ protected override void SolveInstance(IGH_DataAccess DA) DA.SetData(5, program); DA.SetDataList(6, errors); - var isChecked = _form.Play.Checked.HasValue && _form.Play.Checked.Value; - - if (_form.Visible && isChecked) - { - var currentTime = DateTime.Now; - TimeSpan delta = currentTime - _lastTime; - _time += delta.TotalSeconds * _speed; - _lastTime = currentTime; - ExpireSolution(true); - } + Update(); } - // Form - protected override void AppendAdditionalComponentMenuItems(System.Windows.Forms.ToolStripDropDown menu) + internal void TogglePlay() { - Menu_AppendItem(menu, "Open controls", OpenForm, true, _form.Visible); - } - - void OpenForm(object sender, EventArgs e) - { - if (_form.Visible) + if (_lastTime is null) { - _form.Play.Checked = false; - _form.Visible = false; + _lastTime = DateTime.Now; + ExpireSolution(true); } else { - var mousePos = Mouse.Position; - int x = (int)mousePos.X + 20; - int y = (int)mousePos.Y - 160; - - _form.Location = new Point(x, y); - _form.Show(); + Pause(); } } - void ClickPlay(object sender, EventArgs e) + internal void Stop() { - _lastTime = DateTime.Now; + Pause(); + _time = _lastInputTime; ExpireSolution(true); } - void ClickStop(object sender, EventArgs e) + void Pause() { - _form.Play.Checked = false; - _time = _sliderTime; - ExpireSolution(true); + if (_form is not null) + _form.Play.Checked = false; + + _lastTime = null; } - void ClickScroll(object sender, EventArgs e) + void Update() { - _speed = (double)_form.Slider.Value / 100.0; + if (_lastTime is null) + return; + + var currentTime = DateTime.Now; + TimeSpan delta = currentTime - _lastTime.Value; + _lastTime = currentTime; + _time += delta.TotalSeconds * Speed; + ExpireSolution(true); } - public void Dispose() + // form + + public override void CreateAttributes() { - _form.Dispose(); + m_attributes = new ComponentButton(this, "Playback", ToggleForm); } - class AnimForm : Form + public override void RemovedFromDocument(GH_Document document) { - readonly Simulation _component; + base.RemovedFromDocument(document); - internal CheckBox Play; - internal Slider Slider; + if (_form is not null) + _form.Visible = false; + } - public AnimForm(Simulation component) - { - _component = component; - - Maximizable = false; - Minimizable = false; - Padding = new Padding(5); - Resizable = false; - ShowInTaskbar = true; - Topmost = true; - Title = "Playback"; - WindowStyle = WindowStyle.Default; - - var font = new Font(FontFamilies.Sans, 12, FontStyle.None, FontDecoration.None); - var size = new Size(35, 35); - - Play = new CheckBox() - { - Text = "\u25B6", - Size = size, - Font = font, - Checked = false, - TabIndex = 0 - }; - Play.CheckedChanged += component.ClickPlay; - - var stop = new Button() - { - Text = "\u25FC", - Size = size, - Font = font, - TabIndex = 1 - }; - stop.Click += component.ClickStop; - - Slider = new Slider() - { - Orientation = Orientation.Vertical, - Size = new Size(45, 200), - TabIndex = 2, - MaxValue = 400, - MinValue = -200, - TickFrequency = 100, - SnapToTick = true, - Value = 100, - }; - Slider.ValueChanged += _component.ClickScroll; - - var speedLabel = new Label() - { - Text = "100%", - VerticalAlignment = VerticalAlignment.Center, - }; - - var layout = new DynamicLayout(); - layout.BeginVertical(new Padding(2), Size.Empty); - layout.AddSeparateRow(padding: new Padding(10), spacing: new Size(10, 0), controls: new Control[] { Play, stop }); - layout.BeginGroup("Speeds"); - layout.AddSeparateRow(Slider, speedLabel); - layout.EndGroup(); - layout.EndVertical(); - - Content = layout; - } + void ToggleForm() + { + _form ??= new SimulationForm(this); + _form.Visible = !_form.Visible; - protected override void OnClosing(CancelEventArgs e) - { - //base.OnClosing(e); - e.Cancel = true; - Play.Checked = false; - Visible = false; - } + if (!_form.Visible) + Stop(); } -} +} \ No newline at end of file diff --git a/src/Robots.Grasshopper/Program/SimulationForm.cs b/src/Robots.Grasshopper/Program/SimulationForm.cs new file mode 100644 index 0000000..a2339f9 --- /dev/null +++ b/src/Robots.Grasshopper/Program/SimulationForm.cs @@ -0,0 +1,82 @@ +using System.ComponentModel; +using Eto.Drawing; +using Eto.Forms; + +namespace Robots.Grasshopper; + +class SimulationForm : ComponentForm +{ + readonly Simulation _component; + + internal readonly CheckBox Play; + + public SimulationForm(Simulation component) + { + _component = component; + + Title = "Playback"; + MinimumSize = new Size(0, 200); + + Padding = new Padding(5); + + var font = new Font(FontFamilies.Sans, 14, FontStyle.None, FontDecoration.None); + var size = new Size(35, 35); + + Play = new CheckBox + { + Text = "\u25B6", + Size = size, + Font = font, + Checked = false, + TabIndex = 0 + }; + + Play.CheckedChanged += (s, e) => component.TogglePlay(); + + var stop = new Button + { + Text = "\u25FC", + Size = size, + Font = font, + TabIndex = 1 + }; + + stop.Click += (s, e) => component.Stop(); + + var slider = new Slider + { + Orientation = Orientation.Vertical, + Size = new Size(-1, -1), + TabIndex = 2, + MaxValue = 400, + MinValue = -200, + TickFrequency = 100, + SnapToTick = true, + Value = 100, + }; + + slider.ValueChanged += (s, e) => component.Speed = (double)slider.Value / 100.0; ; + + var speedLabel = new Label + { + Text = "100%", + VerticalAlignment = VerticalAlignment.Center, + }; + + var layout = new DynamicLayout(); + layout.BeginVertical(); + layout.AddSeparateRow(padding: new Padding(10), spacing: new Size(10, 0), controls: new Control[] { Play, stop }); + layout.BeginGroup("Speed"); + layout.AddSeparateRow(slider, speedLabel); + layout.EndGroup(); + layout.EndVertical(); + + Content = layout; + } + + protected override void OnClosing(CancelEventArgs e) + { + _component.Stop(); + base.OnClosing(e); + } +} \ No newline at end of file diff --git a/src/Robots.Grasshopper/RobotSystem/LibrariesForm.cs b/src/Robots.Grasshopper/RobotSystem/LibrariesForm.cs new file mode 100644 index 0000000..fcfe016 --- /dev/null +++ b/src/Robots.Grasshopper/RobotSystem/LibrariesForm.cs @@ -0,0 +1,24 @@ +using Eto.Drawing; +using Eto.Forms; + +namespace Robots.Grasshopper; + +class LibrariesForm : ComponentForm +{ + public LibrariesForm() + { + Title = "Robot libraries"; + + Padding = new Padding(5); + MinimumSize = new Size(200, 200); + + Content = new StackLayout + { + Padding = 10, + Items = + { + "Hello World!" + } + }; + } +} \ No newline at end of file diff --git a/src/Robots.Grasshopper/RobotSystem/LoadRobotSystem.cs b/src/Robots.Grasshopper/RobotSystem/LoadRobotSystem.cs index 583a339..45e2369 100644 --- a/src/Robots.Grasshopper/RobotSystem/LoadRobotSystem.cs +++ b/src/Robots.Grasshopper/RobotSystem/LoadRobotSystem.cs @@ -1,5 +1,4 @@ -using System.Drawing; -using Rhino.Geometry; +using Rhino.Geometry; using Grasshopper; using Grasshopper.Kernel; using Grasshopper.Kernel.Types; @@ -9,13 +8,14 @@ namespace Robots.Grasshopper; public class LoadRobotSystem : GH_Component { + LibrariesForm? _form; GH_ValueList? _valueList = null; IGH_Param? _parameter = null; public LoadRobotSystem() : base("Load robot system", "LoadRobot", "Loads a robot system from the library.", "Robots", "Components") { } public override GH_Exposure Exposure => GH_Exposure.primary; public override Guid ComponentGuid => new("{7722D7E3-98DE-49B5-9B1D-E0D1B938B4A7}"); - protected override Bitmap Icon => Util.GetIcon("iconRobot"); + protected override System.Drawing.Bitmap Icon => Util.GetIcon("iconRobot"); protected override void RegisterInputParams(GH_InputParamManager pManager) { @@ -43,7 +43,7 @@ protected override void BeforeSolveInstance() if (inputValueList is null) { _valueList.CreateAttributes(); - _valueList.Attributes.Pivot = new PointF(Attributes.Pivot.X - 180, Attributes.Pivot.Y - 31); + _valueList.Attributes.Pivot = new System.Drawing.PointF(Attributes.Pivot.X - 180, Attributes.Pivot.Y - 31); AddRobotsToValueList(_valueList); Instances.ActiveCanvas.Document.AddObject(_valueList, false); _parameter.AddSource(_valueList); @@ -88,4 +88,25 @@ protected override void SolveInstance(IGH_DataAccess DA) var robotSystem = FileIO.LoadRobotSystem(name, basePlane.Value); DA.SetData(0, new GH_RobotSystem(robotSystem)); } -} + + // form + + public override void CreateAttributes() + { + m_attributes = new ComponentButton(this, "Libraries", ToggleForm); + } + + public override void RemovedFromDocument(GH_Document document) + { + base.RemovedFromDocument(document); + + if (_form is not null) + _form.Visible = false; + } + + void ToggleForm() + { + _form ??= new LibrariesForm(); + _form.Visible = !_form.Visible; + } +} \ No newline at end of file diff --git a/src/Robots/FileIO.cs b/src/Robots/IO/FileIO.cs similarity index 85% rename from src/Robots/FileIO.cs rename to src/Robots/IO/FileIO.cs index 0c96617..6b11370 100644 --- a/src/Robots/FileIO.cs +++ b/src/Robots/IO/FileIO.cs @@ -1,9 +1,7 @@ -using System.Text.RegularExpressions; -using System.Xml; +using System.Xml; using System.Xml.Linq; using Rhino.FileIO; using Rhino.Geometry; -using static Robots.Util; namespace Robots; @@ -30,17 +28,67 @@ public static Tool LoadTool(string name) return CreateTool(element); } - static List List(string type) + // library files + + /// + /// Win: C:\Users\userName\Documents\Robots + /// Mac: /Users/userName/Robots + /// + public static string LocalLibraryPath => + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Robots"); + + /// + /// Win: C:\Users\userName\AppData\Roaming\McNeel\Rhinoceros\packages\7.0\Robots\libraries + /// Mac: /Users/userName/.config/McNeel/Rhinoceros/packages/7.0/Robots/libraries + /// Lib: {appData}\Robots\libraries + /// + public static string OnlineLibraryPath + { + get + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.DoNotVerify); +#if (NET48 || DEBUG) + return Path.Combine(appData, "McNeel", "Rhinoceros", "packages", "7.0", "Robots", "libraries"); +#elif NETSTANDARD2_0 + return Path.Combine(appData, "Robots", "libraries"); +#endif + } + } + + static IEnumerable GetLibraryPaths() { - string folder = LibraryPath; + yield return LocalLibraryPath; + yield return OnlineLibraryPath; + } + + static IEnumerable GetLibraries() + { + var previous = new HashSet(); + + foreach (var path in GetLibraryPaths()) + { + if (!Directory.Exists(path)) + continue; + + var files = Directory.EnumerateFiles(path, "*.xml"); + + foreach (var file in files) + { + var name = Path.GetFileNameWithoutExtension(file); - if (!Directory.Exists(folder)) - throw new DirectoryNotFoundException($" Folder '{folder}' not found."); + if (!previous.Add(name)) + continue; + yield return file; + } + } + } + + static List List(string type) + { var names = new List(); - var files = Directory.GetFiles(folder, "*.xml"); - foreach (var file in files) + foreach (var file in GetLibraries()) { var root = XElement.Load(file); @@ -53,16 +101,9 @@ static List List(string type) static XElement LoadElement(string name, string type) { - string folder = LibraryPath; - - if (!Directory.Exists(folder)) - throw new DirectoryNotFoundException($" Folder '{folder}' not found"); - - var files = Directory.GetFiles(folder, "*.xml"); - - foreach (var file in files) + foreach (var file in GetLibraries()) { - XElement root = XElement.Load(file); + var root = XElement.Load(file); var element = root.Elements(XName.Get(type)) ?.FirstOrDefault(e => e.GetAttribute("name") == name); @@ -246,27 +287,18 @@ static Plane CreatePlane(XElement element) static File3dm GetRhinoDoc(string name, string type) { - string folder = LibraryPath; - - if (!Directory.Exists(folder)) - throw new DirectoryNotFoundException($" Folder '{folder}' not found"); - - var files = Directory.GetFiles(folder, "*.xml"); - - foreach (var file in files) + foreach (var file in GetLibraries()) { - XElement root = XElement.Load(file); + var root = XElement.Load(file); var element = root.Elements(XName.Get(type))?.FirstOrDefault(e => e.GetAttribute("name") == name); if (element is null) continue; - var dir = Path.GetDirectoryName(file); - var file3dm = $"{Path.GetFileNameWithoutExtension(file)}.3dm"; - var path3dm = Path.Combine(dir, file3dm); + var path3dm = Path.ChangeExtension(file, ".3dm"); if (!File.Exists(path3dm)) - throw new FileNotFoundException($" File '{file3dm}' not found"); + throw new FileNotFoundException($" File '{Path.GetFileName(path3dm)}' not found"); return File3dm.Read(path3dm); } @@ -323,38 +355,6 @@ static Mesh GetToolMesh(string name) // Extensions - public static bool IsValidName(this string name, out string error) - { - if (name.Length == 0) - { - error = "name is empty."; - return false; - } - - var excess = name.Length - 32; - - if (excess > 0) - { - error = $"name is {excess} character(s) too long."; - return false; - } - - if (!char.IsLetter(name[0])) - { - error = "name must start with a letter."; - return false; - } - - if (!Regex.IsMatch(name, @"^[A-Z0-9_]+$", RegexOptions.IgnoreCase)) - { - error = "name can only contain letters, digits, and underscores (_)."; - return false; - } - - error = ""; - return true; - } - static XElement GetElement(this XElement element, string name) { return element.Element(XName.Get(name)) diff --git a/src/Robots/IO/OnlineLibrary.cs b/src/Robots/IO/OnlineLibrary.cs new file mode 100644 index 0000000..ac0cce1 --- /dev/null +++ b/src/Robots/IO/OnlineLibrary.cs @@ -0,0 +1,155 @@ +using Octokit; +using System.Security.Cryptography; + +namespace Robots; + +public class LibraryItem +{ + public string Name { get; set; } = default!; + public bool IsLocal { get; set; } + internal string? OnlineSha { get; set; } + internal string? DownloadedSha { get; set; } + + public bool IsOnline => OnlineSha is not null; + public bool IsDownloaded => DownloadedSha is not null; + public bool IsUpdateAvailable => IsOnline && (OnlineSha != DownloadedSha); +} + +public class OnlineLibrary +{ + readonly HttpClient _http = new(); + public Dictionary Libraries { get; } = new(); + + public OnlineLibrary() + { + _http.BaseAddress = new Uri("https://raw.githubusercontent.com/visose/Robots/master/libraries/"); + } + + public async Task UpdateLibraryAsync() + { + Libraries.Clear(); + await AddOnlineLibrariesAsync(); + AddDiskLibraries(FileIO.OnlineLibraryPath, false); + AddDiskLibraries(FileIO.LocalLibraryPath, true); + } + + public async Task TryDownloadLibraryAsync(LibraryItem library) + { + if (!library.IsUpdateAvailable) + throw new ArgumentException("Library does not require update."); + + if (!await TryDownloadFileAsync(library.Name + ".xml")) + return false; + + if (!await TryDownloadFileAsync(library.Name + ".3dm")) + return false; + + return true; + } + + async Task AddOnlineLibrariesAsync() + { + var github = new GitHubClient(new ProductHeaderValue("visoseRobots")); + var files = await github.Repository.Content.GetAllContents("visose", "Robots", "libraries"); + + foreach (var file in files) + { + var extension = GetValidExtension(file.Name); + + if (extension is null) + continue; + + var name = Path.GetFileNameWithoutExtension(file.Name); + + if (!Libraries.TryGetValue(name, out var value)) + { + value = new LibraryItem { Name = name }; + Libraries.Add(name, value); + } + + var sha = value.OnlineSha; + value.OnlineSha = extension switch + { + ".xml" => string.Concat(file.Sha, sha), + ".3dm" => string.Concat(sha, file.Sha), + _ => throw new ArgumentException("Wrong extension."), + }; + } + } + + string? GetValidExtension(string fileName) + { + foreach (var extension in new[] { ".xml", ".3dm" }) + { + if (Path.GetExtension(fileName).Equals(extension, StringComparison.InvariantCultureIgnoreCase)) + return extension; + } + + return null; + } + + void AddDiskLibraries(string folder, bool isLocal) + { + if (!Directory.Exists(folder)) + return; + + var files = Directory.EnumerateFiles(folder, "*.xml"); + + foreach (var file in files) + { + var name = Path.GetFileNameWithoutExtension(file); + + if (!Libraries.TryGetValue(name, out var value)) + { + value = new LibraryItem { Name = name }; + Libraries.Add(name, value); + } + + var shaXml = GetSha1(file); + var sha3dm = GetSha1(Path.ChangeExtension(file, ".3dm")); + value.DownloadedSha = string.Concat(shaXml, sha3dm); + + if (isLocal) + value.IsLocal = true; + } + } + + string GetSha1(string file) + { + var bytes = File.ReadAllBytes(file); + return GetSha1(bytes); + } + + string GetSha1(byte[] contentBytes) + { + var header = $"blob {contentBytes.Length}\0"; + var encoding = new System.Text.UTF8Encoding(); + var headerBytes = encoding.GetBytes(header); + + var bytes = Util.Combine(headerBytes, contentBytes); + + var sha1 = SHA1.Create(); + var hash = sha1.ComputeHash(bytes); + var hashText = string.Concat(hash.Select(b => b.ToString("x2"))); + return hashText; + } + + async Task TryDownloadFileAsync(string fileName) + { + var folder = FileIO.OnlineLibraryPath; + + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder); + + string downloadPath = Path.Combine(FileIO.OnlineLibraryPath, fileName); + var response = await _http.GetAsync(fileName); + + if (response.StatusCode != System.Net.HttpStatusCode.OK) + return false; + + var bytes = await response.Content.ReadAsByteArrayAsync(); + File.WriteAllBytes(downloadPath, bytes); + + return true; + } +} \ No newline at end of file diff --git a/src/Robots/Program/CheckProgram.cs b/src/Robots/Program/CheckProgram.cs index 9efeec7..7d1d283 100644 --- a/src/Robots/Program/CheckProgram.cs +++ b/src/Robots/Program/CheckProgram.cs @@ -31,10 +31,10 @@ void CheckName() if (_robotSystem is RobotCell cell) { var group = cell.MechanicalGroups.MaxBy(g => g.Name.Length).Name; - name = $"{name}_{group}_{000}"; + name = $"{name}_{group}_000"; } - - if (!name.IsValidName(out var error)) + + if (!Program.IsValidIdentifier(name, out var error)) _program.Errors.Add("Program " + error); if (_robotSystem is RobotCellKuka) diff --git a/src/Robots/Program/Collision.cs b/src/Robots/Program/Collision.cs index 9ea8ee1..200ec85 100644 --- a/src/Robots/Program/Collision.cs +++ b/src/Robots/Program/Collision.cs @@ -15,7 +15,7 @@ internal Collision(Program program, IEnumerable first, IEnumerable sec throw NotImplemented(); } - Exception NotImplemented() => new NotImplementedException(" Collisions have to be reimplemented."); + Exception NotImplemented() => new NotImplementedException(" Collisions not implemented in standalone."); } #elif NET48 diff --git a/src/Robots/Program/Program.cs b/src/Robots/Program/Program.cs index 6bfd115..d4a37a2 100644 --- a/src/Robots/Program/Program.cs +++ b/src/Robots/Program/Program.cs @@ -1,4 +1,5 @@ using Rhino.Geometry; +using System.Text.RegularExpressions; using static System.Math; namespace Robots; @@ -14,6 +15,41 @@ public interface IProgram public class Program : IProgram { + // static + public static bool IsValidIdentifier(string name, out string error) + { + if (name.Length == 0) + { + error = "name is empty."; + return false; + } + + var excess = name.Length - 32; + + if (excess > 0) + { + error = $"name is {excess} character(s) too long."; + return false; + } + + if (!char.IsLetter(name[0])) + { + error = "name must start with a letter."; + return false; + } + + if (!Regex.IsMatch(name, @"^[A-Z0-9_]+$", RegexOptions.IgnoreCase)) + { + error = "name can only contain letters, digits, and underscores (_)."; + return false; + } + + error = ""; + return true; + } + + // instance + readonly Simulation _simulation; public string Name { get; } diff --git a/src/Robots/Remotes/RemoteAbb.cs b/src/Robots/Remotes/RemoteAbb.cs index 673f52d..c08da58 100644 --- a/src/Robots/Remotes/RemoteAbb.cs +++ b/src/Robots/Remotes/RemoteAbb.cs @@ -129,12 +129,12 @@ string UploadCommand(IProgram program) if (_controller is null) return "Controller is null."; - string tempPath = Path.Combine(Util.LibraryPath, "temp"); + string tempPath = Path.Combine(Path.GetTempPath(), "Robots"); try { if (Directory.Exists(tempPath)) - Directory.Delete(tempPath, true); + Directory.Delete(tempPath, true); Directory.CreateDirectory(tempPath); program.Save(tempPath); @@ -147,7 +147,7 @@ string UploadCommand(IProgram program) _controller.FileSystem.PutDirectory(localFolder, program.Name, true); using Mastership master = Mastership.Request(_controller); - var task = _controller.Rapid.GetTasks().First(); + using var task = _controller.Rapid.GetTasks().First(); task.DeleteProgram(); int count = 0; diff --git a/src/Robots/Robots.csproj b/src/Robots/Robots.csproj index 8d06b43..3b26df5 100644 --- a/src/Robots/Robots.csproj +++ b/src/Robots/Robots.csproj @@ -1,5 +1,5 @@  - + netstandard2.0;net48 $(Product) Core @@ -16,8 +16,12 @@ + + + + - + \ No newline at end of file diff --git a/src/Robots/TargetAttributes/TargetAttribute.cs b/src/Robots/TargetAttributes/TargetAttribute.cs index 9c901e6..0dac986 100644 --- a/src/Robots/TargetAttributes/TargetAttribute.cs +++ b/src/Robots/TargetAttributes/TargetAttribute.cs @@ -18,7 +18,7 @@ public string Name get => _name.NotNull(); private set { - if (!value.IsValidName(out var error)) + if (!Program.IsValidIdentifier(value, out var error)) throw new ArgumentException($" {GetType().Name} {error}"); _name = value; diff --git a/src/Robots/Util.cs b/src/Robots/Util.cs index 85b1cc8..b62c60b 100644 --- a/src/Robots/Util.cs +++ b/src/Robots/Util.cs @@ -15,10 +15,6 @@ static class Util public const double HalfPI = PI * 0.5; public const double PI2 = PI * 2.0; - // File - - public static string LibraryPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Robots"); - // Exceptions public static T NotNull(this T? value, string? text = null) @@ -98,8 +94,8 @@ public static T MaxBy(this IEnumerable list, Func comparable) whe if (val.CompareTo(maxValue) > 0) { - maxValue = val; maxItem = item; + maxValue = val; } } @@ -123,6 +119,20 @@ public static IEnumerable> Transpose(this IEnumerable> } } + public static byte[] Combine(params byte[][] arrays) + { + var result = new byte[arrays.Sum(x => x.Length)]; + int offset = 0; + + foreach (byte[] data in arrays) + { + Buffer.BlockCopy(data, 0, result, offset, data.Length); + offset += data.Length; + } + + return result; + } + // Geometry public static double ToRadians(this double value) diff --git a/tests/Robots.Tests/Performance.cs b/tests/Robots.Tests/Performance.cs new file mode 100644 index 0000000..3b4499a --- /dev/null +++ b/tests/Robots.Tests/Performance.cs @@ -0,0 +1,90 @@ +using System.Diagnostics; +using Rhino.Geometry; + +namespace Robots.Tests; + +class Performance +{ + readonly Stopwatch _watch = new(); + readonly Dictionary _times = new(); + + public Performance() + { + + int count = 20; + + for (int i = 0; i < count; i++) + PerfTestAbb(); + //PerfTestUR(); + + foreach (var time in _times) + Console.WriteLine($"{time.Key}: {time.Value / (count - 1)} ms"); + } + + void Log(string key) + { + _watch.Stop(); + long ms = _watch.ElapsedMilliseconds; + _times[key] = _times.ContainsKey(key) + ? _times[key] + ms + : 0; + + _watch.Restart(); + } + + void PerfTestAbb() + { + _watch.Restart(); + const string xml = ""; + var robot = FileIO.ParseRobotSystem(xml, Plane.WorldXY); + + Log("RobotSytem.Parse"); + + var planeA = Plane.WorldYZ; + var planeB = Plane.WorldYZ; + planeA.Origin = new Point3d(300, 200, 610); + planeB.Origin = new Point3d(300, -200, 610); + var speed = new Speed(300); + var targetA = new JointTarget(new[] { 0, Math.PI * 0.5, 0, 0, 0, 0 }); + var targetB = new CartesianTarget(planeA, RobotConfigurations.Wrist, Motions.Joint); + var targetC = new CartesianTarget(planeB, null, Motions.Linear, speed: speed); + var toolpath = new SimpleToolpath() { targetA, targetB, targetC }; + + Log("Toolpath"); + + var program = new Robots.Program("TestProgram", robot, new[] { toolpath }, stepSize: 0.02); + + Log("Program"); // 486 + + double expected = 3.7851345985264309; + Debug.Assert(program.Duration == expected, "Test failed"); + } + + void PerfTestUR() + { + _watch.Restart(); + const string xml = ""; + var robot = FileIO.ParseRobotSystem(xml, Plane.WorldXY); + + Log("RobotSytem.Parse"); + + var planeA = Plane.WorldZX; + var planeB = Plane.WorldZX; + planeA.Origin = new Point3d(200, 100, 600); + planeB.Origin = new Point3d(700, 250, 600); + var speed = new Speed(300); + var targetA = new CartesianTarget(planeA, RobotConfigurations.Wrist, Motions.Joint); + var targetB = new CartesianTarget(planeB, null, Motions.Linear, speed: speed); + var toolpath = new SimpleToolpath() { targetA, targetB }; + + Log("Toolpath"); + + var program = new Robots.Program("URTest", robot, new[] { toolpath }, stepSize: 0.01); + + Log("Program"); + + double expected = 1.7425724724517486; + var err = Math.Abs(program.Duration - expected); + Debug.Assert(err < 1e-9, "Test failed"); + } +} \ No newline at end of file diff --git a/tests/Robots.Tests/Program.cs b/tests/Robots.Tests/Program.cs index 87d2803..4b63708 100644 --- a/tests/Robots.Tests/Program.cs +++ b/tests/Robots.Tests/Program.cs @@ -1,84 +1,8 @@ -using System.Diagnostics; -using Rhino.Geometry; +using Robots.Tests; using Robots; -//var mesh = new Mesh(); -Dictionary times = new(); -Stopwatch watch = new(); -int count = 20; - -for (int i = 0; i < count; i++) - PerfTestAbb(); - //PerfTestUR(); - -foreach (var time in times) - Console.WriteLine($"{time.Key}: {time.Value / (count-1)} ms"); - //dotnet run --property:Configuration=Release +//new Performance(); -void Log(string key) -{ - watch.Stop(); - long ms = watch.ElapsedMilliseconds; - times[key] = times.ContainsKey(key) - ? times[key] + ms - : 0; - - watch.Restart(); -} - -void PerfTestAbb() -{ - watch.Restart(); - const string xml = ""; - var robot = FileIO.ParseRobotSystem(xml, Plane.WorldXY); - - Log("RobotSytem.Parse"); - - var planeA = Plane.WorldYZ; - var planeB = Plane.WorldYZ; - planeA.Origin = new Point3d(300, 200, 610); - planeB.Origin = new Point3d(300, -200, 610); - var speed = new Speed(300); - var targetA = new JointTarget(new[] { 0, Math.PI * 0.5, 0, 0, 0, 0 }); - var targetB = new CartesianTarget(planeA, RobotConfigurations.Wrist, Motions.Joint); - var targetC = new CartesianTarget(planeB, null, Motions.Linear, speed: speed); - var toolpath = new SimpleToolpath() { targetA, targetB, targetC }; - - Log("Toolpath"); - - var program = new Robots.Program("TestProgram", robot, new[] { toolpath }, stepSize: 0.02); - - Log("Program"); // 486 - - double expected = 3.7851345985264309; - Debug.Assert(program.Duration == expected, "Test failed"); -} - -void PerfTestUR() -{ - watch.Restart(); - const string xml = ""; - var robot = FileIO.ParseRobotSystem(xml, Plane.WorldXY); - - Log("RobotSytem.Parse"); - - var planeA = Plane.WorldZX; - var planeB = Plane.WorldZX; - planeA.Origin = new Point3d(200, 100, 600); - planeB.Origin = new Point3d(700, 250, 600); - var speed = new Speed(300); - var targetA = new CartesianTarget(planeA, RobotConfigurations.Wrist, Motions.Joint); - var targetB = new CartesianTarget(planeB, null, Motions.Linear, speed: speed); - var toolpath = new SimpleToolpath() { targetA, targetB }; - - Log("Toolpath"); - - var program = new Robots.Program("URTest", robot, new[] { toolpath }, stepSize: 0.01); - - Log("Program"); - - double expected = 1.7425724724517486; - var err = Math.Abs(program.Duration - expected); - Debug.Assert(err < 1e-9, "Test failed"); -} \ No newline at end of file +//var github = new OnlineLibrary(); +//await github.UpdateLibraryAsync();