diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..cb4905b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(ls:*)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..37d1bc9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +VisionBuilder.UI is a modular .NET 8.0 WinForms application for industrial computer vision. It processes images through configurable operation pipelines, supports AI inference (YOLO/ONNX), and integrates with HawkEye camera hardware. The solution uses Ninject for DI and follows MVVM patterns. + +## Build Commands + +```bash +dotnet build VisionBuilder.UI.sln -c Debug # GPU build (YoloV8.Gpu + OnnxRuntime.Gpu) +dotnet build VisionBuilder.UI.sln -c CPU # CPU-only build (YoloV8 + OnnxRuntime) +dotnet build VisionBuilder.UI.sln -c Release # Production build +``` + +The `Debug` and `Release` configurations pull GPU NuGet packages; `CPU` configuration pulls CPU-only packages. This is controlled by conditional `` blocks in `Hawkeye.VisionBuilder.Workflow.csproj`. + +## Testing + +Test framework: **MSTest** (Microsoft.NET.Test.Sdk 17.12.0, MSTest 3.6.4) + +```bash +dotnet test VisionBuilder.UI.sln # Run all tests +dotnet test VisionBuilder.UI.Tests/ # Primary unit tests only +dotnet test --filter "FullyQualifiedName~ClassName.Method" # Single test +``` + +Test projects: `VisionBuilder.UI.Tests`, `VisionBuilder.UI.Recipes.Scripted.Tests`, `IDSTest` + +## Solution Architecture + +The solution is organized into logical groups: + +**Starters** (entry points): +- `Hawkeye.VisionBuilder` — Main WinExe. `Program.cs` bootstraps the Ninject kernel, creates the `WorkflowList` and `OperationDiscoveryService`, then runs `MainWindow`. + +**Core Modules**: +- `Hawkeye.VisionBuilder.Workflow` — Image processing pipeline engine. Contains `BaseOperation`, `Context`, `WorkflowList`, `OperationDiscoveryService`, and all operation implementations. See its own `CLAUDE.md` for detailed operation/recipe documentation. +- `VisionBuilder.UI.Common` — Shared contracts: `IPlugin`, `IVisionBuilderModule`, `IRecognitionControl`, `IImageSource`, service interfaces, MVVM ViewModels, command/event system. +- `VisionBuilder.UI.Windows` — WinForms UI controls (PreviewWindow, ErrorPreview, Stats, SingleCameraControl). + +**Framework** (reusable libraries under `framework/`): +- `Inspectron.Camera`, `Inspectron.Settings`, `Inspectron.HawkEye`, `Inspectron.Fastbuffer`, `Inspectron.Ringbuffer`, `MaterialSkin.Core` — Hardware abstraction, settings management, and UI theming. Some target `netstandard2.1`. + +**Sources** (image input providers): +- `Sources.Emulation` — File-based emulated camera for development. +- `Sources.Hawkeye` — Real HawkEye hardware source. +- `Sources.IDS` — IDS industrial camera integration. + +**Plugins** (extend functionality via `IPlugin` interface): +- `B24SiemensPlugin`, `CandyboxPlugin`, `PralinenPLC`, `PackstrasseBarcodeReader`, etc. +- Plugins register services into the Ninject kernel at global and per-camera scope. + +**Recipes** (workflow serialization): +- `Recipes.HawkeyeRecipe` — Binary `.hrcp` and XML `.xhrcp` recipe formats. +- `Recipes.Scripted` — Script-based recipe execution. + +## Key Architectural Patterns + +### Operation Pipeline +Images flow through a chain of `BaseOperation` subclasses. Each operation receives a `Context` (holding `ActiveImage`, `Memory` dictionary, `GraphicsElements`) and implements `InterpretInternal(Context)`. Operations are discovered at runtime via reflection by `OperationDiscoveryService`. Categories: `AI/`, `Simple/`, `Filters/`, `Morphology/`, `Image/`, `Basic/`. + +### Plugin System +Plugins implement `IPlugin` with two registration points: +- `RegisterGlobalModules(IKernel)` — app-wide services +- `RegisterCameraModules(IKernel, string cameraName)` — per-camera services + +Modules implement `IVisionBuilderModule.InitializeModule()` for deferred initialization. + +### DI Container +Ninject `StandardKernel` is the root container. Child kernels (`Ninject.Extensions.ChildKernel`) scope per-camera services. All service resolution flows through the kernel — avoid `new` for services. + +### Event/Command System +`IEvent` / `IEventHandler` in `VisionBuilder.UI.Common/Commands/`. Key events: `SessionStartedEvent`, `SessionEndedEvent`, `ImageProcessedEvent`, `ErrorsInSequenceEvent`. `IRecognitionControl` surfaces these as `Action` events. + +## Key Dependencies + +- **OpenCvSharp4** — Core image processing +- **YoloV8 / YoloV8.Gpu** — AI object detection (conditional on build config) +- **IronPython 3.4.0** — Python scripting integration +- **Ninject** — Dependency injection +- **CommunityToolkit.Mvvm** — MVVM support (ObservableObject, RelayCommand) +- **Serilog** — Structured logging +- **Scintilla.NET** — Code editor component in UI +- **MaterialSkin.Core** — Material Design WinForms theming + +## Conventions + +- Target platform is **x64** Windows. +- Culture is forced to `en-US` at startup. +- Nullable reference types are enabled across most projects. +- Operation attributes: `[Category("name")]` for UI grouping, `[NotForTool]` to exclude properties from serialization, `[IgnoreOperation]` to hide from discovery. +- Recipe serialization supports both binary (`.hrcp`) and XML (`.xhrcp`) formats. Prefer XML for new work. Override `SaveXML`/`LoadXML`/`GetOperationElementName` for custom serialization. diff --git a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs index 36027e5..a79cf47 100644 --- a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs +++ b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs @@ -1,6 +1,7 @@ using Hawkeye.VisionBuilder.Workflow.Configuration; using Hawkeye.VisionBuilder.Workflow.Links; using OpenCvSharp; +using Serilog; using System.ComponentModel.Design.Serialization; using System.Diagnostics; using System.Drawing; @@ -472,4 +473,77 @@ public class WorkflowList return workflowList; } + public void HotReloadParameters(string fileName) + { + string jsonString = File.ReadAllText(fileName); + var workflowData = JsonSerializer.Deserialize>(jsonString); + + if (workflowData == null) return; + + // Update Configuration + if (workflowData.ContainsKey("Configuration")) + { + var configElement = (JsonElement)workflowData["Configuration"]; + var configData = JsonSerializer.Deserialize>(configElement.GetRawText()); + + if (configData != null) + { + var convertedConfigData = ConvertJsonElementsToNatives(configData); + + if (convertedConfigData.ContainsKey("RuntimeCameraType")) + Configuration.RuntimeCameraType = Enum.Parse(convertedConfigData["RuntimeCameraType"].ToString()); + if (convertedConfigData.ContainsKey("DevelopmentCameraType")) + Configuration.DevelopmentCameraType = Enum.Parse(convertedConfigData["DevelopmentCameraType"].ToString()); + if (convertedConfigData.ContainsKey("Outputs")) + Configuration.Outputs = Enum.Parse(convertedConfigData["Outputs"].ToString()); + if (convertedConfigData.ContainsKey("EmulationPath")) + Configuration.EmulationPath = convertedConfigData["EmulationPath"].ToString(); + if (convertedConfigData.ContainsKey("PythonPath")) + Configuration.PythonPath = convertedConfigData["PythonPath"].ToString(); + if (convertedConfigData.ContainsKey("ResultPin")) + Configuration.ResultPin = (int)convertedConfigData["ResultPin"]; + if (convertedConfigData.ContainsKey("SerialPort")) + Configuration.SerialPort = convertedConfigData["SerialPort"].ToString(); + if (convertedConfigData.ContainsKey("Delay")) + Configuration.Delay = (int)convertedConfigData["Delay"]; + } + } + + // Update operation parameters by matching Id — do NOT recreate the Operations list + if (workflowData.ContainsKey("Operations")) + { + var operationsElement = (JsonElement)workflowData["Operations"]; + var operationsArray = JsonSerializer.Deserialize>>(operationsElement.GetRawText()); + + if (operationsArray != null) + { + foreach (var operationData in operationsArray) + { + if (!operationData.ContainsKey("Data")) continue; + + var dataElement = (JsonElement)operationData["Data"]; + var operationDict = JsonSerializer.Deserialize>(dataElement.GetRawText()); + + if (operationDict == null) continue; + + var convertedDict = ConvertJsonElementsToNatives(operationDict); + + if (!convertedDict.ContainsKey("Id")) continue; + + var id = new Guid(convertedDict["Id"].ToString()); + var existingOp = Operations.FirstOrDefault(x => x.Id == id); + + if (existingOp != null) + { + existingOp.Load(convertedDict); + } + else + { + Log.Warning("HotReload: no matching operation for Id {Id}", id); + } + } + } + } + } + } \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Processing/BaseRecognitionControl.cs b/VisionBuilder.UI.Common/Processing/BaseRecognitionControl.cs index 24c2d18..a996fb2 100644 --- a/VisionBuilder.UI.Common/Processing/BaseRecognitionControl.cs +++ b/VisionBuilder.UI.Common/Processing/BaseRecognitionControl.cs @@ -142,6 +142,7 @@ public abstract class BaseRecognitionControl: IRecognitionControl _cts?.Dispose(); _cts = null; _loopTask = null; + Cleanup(); } SessionEnded?.Invoke(new SessionEndedEvent @@ -241,4 +242,9 @@ public abstract class BaseRecognitionControl: IRecognitionControl public event Action SessionStarted = delegate { }; public event Action SessionEnded = delegate { }; public event Action ErrorsInSequenceAlarm = delegate { }; + + public event Action RecipeFileChanged = delegate { }; + public virtual void HotReload() { } + protected void RaiseRecipeFileChanged() => RecipeFileChanged?.Invoke(); + protected virtual void Cleanup() { } } \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Processing/IRecognitionControl.cs b/VisionBuilder.UI.Common/Processing/IRecognitionControl.cs index 2dca183..f709ef6 100644 --- a/VisionBuilder.UI.Common/Processing/IRecognitionControl.cs +++ b/VisionBuilder.UI.Common/Processing/IRecognitionControl.cs @@ -17,4 +17,6 @@ public interface IRecognitionControl event Action SessionEnded; event Action ErrorsInSequenceAlarm; + void HotReload(); + event Action RecipeFileChanged; } \ No newline at end of file diff --git a/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs b/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs index 9157da1..ec295a4 100644 --- a/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs +++ b/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs @@ -32,9 +32,11 @@ namespace VisionBuilder.UI.Common [NotifyPropertyChangedFor(nameof(CanStop))] [NotifyPropertyChangedFor(nameof(CanPause))] [NotifyPropertyChangedFor(nameof(CanResume))] + [NotifyPropertyChangedFor(nameof(CanHotReload))] [NotifyCanExecuteChangedFor(nameof(SelectRecipeCommand))] [NotifyCanExecuteChangedFor(nameof(StartCommand))] [NotifyCanExecuteChangedFor(nameof(StopCommand))] + [NotifyCanExecuteChangedFor(nameof(HotReloadCommand))] private bool _isRunning; [ObservableProperty] @@ -55,6 +57,13 @@ namespace VisionBuilder.UI.Common public bool IsResumed => !IsPaused; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanHotReload))] + [NotifyCanExecuteChangedFor(nameof(HotReloadCommand))] + private bool _isHotReloadAvailable; + + public bool CanHotReload => IsRunning && IsHotReloadAvailable; + public bool CanStop => IsRunning; public bool CanStart => !IsRunning && SelectedRecipe!=null; public bool CanPause => (IsRunning && !IsPaused)&&_uiConfiguration.ShowPauseButton; @@ -100,6 +109,9 @@ namespace VisionBuilder.UI.Common recognitionControl.SessionStarted += StatisticsVm.Handle; recognitionControl.SessionStarted += ErrorsVm.Handle; + recognitionControl.RecipeFileChanged += () => + SynchronizationContext?.Post(_ => IsHotReloadAvailable = true, null); + } @@ -153,7 +165,14 @@ namespace VisionBuilder.UI.Common }); IsPaused = false; IsRunning = false; + IsHotReloadAvailable = false; + } + [RelayCommand(CanExecute = nameof(CanHotReload))] + public void HotReload() + { + _recognitionControl.HotReload(); + IsHotReloadAvailable = false; } [RelayCommand] diff --git a/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs b/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs index 01eeefb..3f2253c 100644 --- a/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs +++ b/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs @@ -17,11 +17,16 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe { private readonly IImageSource _imageSource; private readonly HawkeyeRecognitionSettings _recognitionConfiguration; - + private CancellationTokenSource _cts; private Task _loopTask; + private FileSystemWatcher? _fileWatcher; + private System.Timers.Timer? _debounceTimer; + private string? _pendingHotReloadFile; + private string? _currentRecipeFilePath; + public HawkeyeRecognitionControl(IImageSource imageSource, HawkeyeRecognitionSettings recognitionConfiguration, ILoadingService loadingService): base(recognitionConfiguration,loadingService) { _imageSource = imageSource; @@ -62,7 +67,10 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe protected override void Initialize(RecipeData currentRecipe) { - _workflow = WorkflowList.LoadJSONFromFile("..\\Data\\Recipes\\" + currentRecipe.RecipeName + ".jhrcp"); + var path = "..\\Data\\Recipes\\" + currentRecipe.RecipeName + ".jhrcp"; + _currentRecipeFilePath = Path.GetFullPath(path); + _workflow = WorkflowList.LoadJSONFromFile(path); + StartFileWatcher(_currentRecipeFilePath); } protected override void WarmUp() @@ -75,7 +83,10 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe protected override (Mat originalImage, Mat analysisImage,TimeSpan processingTime, TimeSpan acquisitionTime, string[] errorNames)? ProcessImage(CancellationToken token) { - + var pendingFile = Interlocked.Exchange(ref _pendingHotReloadFile, null); + if (pendingFile != null) + _workflow.HotReloadParameters(pendingFile); + var sw = Stopwatch.StartNew(); _workflow.Context.CancellationToken = token; _workflow.ImageSource = _imageSource; @@ -113,5 +124,52 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe return (originalImage, analysisImage, sw.Elapsed, cameraTime, errorNames); } + private void StartFileWatcher(string filePath) + { + StopFileWatcher(); + + var directory = Path.GetDirectoryName(filePath)!; + var fileName = Path.GetFileName(filePath); + + _debounceTimer = new System.Timers.Timer(500) { AutoReset = false }; + _debounceTimer.Elapsed += (_, _) => RaiseRecipeFileChanged(); + + _fileWatcher = new FileSystemWatcher(directory, fileName) + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size, + EnableRaisingEvents = true + }; + _fileWatcher.Changed += (_, _) => + { + _debounceTimer.Stop(); + _debounceTimer.Start(); + }; + } + + private void StopFileWatcher() + { + if (_fileWatcher != null) + { + _fileWatcher.EnableRaisingEvents = false; + _fileWatcher.Dispose(); + _fileWatcher = null; + } + if (_debounceTimer != null) + { + _debounceTimer.Stop(); + _debounceTimer.Dispose(); + _debounceTimer = null; + } + } + + public override void HotReload() + { + Interlocked.Exchange(ref _pendingHotReloadFile, _currentRecipeFilePath); + } + + protected override void Cleanup() + { + StopFileWatcher(); + } } } diff --git a/VisionBuilder.UI.Windows/Components/SingleCameraControl.Designer.cs b/VisionBuilder.UI.Windows/Components/SingleCameraControl.Designer.cs index 93ca2b6..259c459 100644 --- a/VisionBuilder.UI.Windows/Components/SingleCameraControl.Designer.cs +++ b/VisionBuilder.UI.Windows/Components/SingleCameraControl.Designer.cs @@ -40,8 +40,9 @@ btnSelectRecipe = new MaterialSkin.Controls.MaterialRaisedButton(); btnStop = new MaterialSkin.Controls.MaterialRaisedButton(); btnPause = new MaterialSkin.Controls.MaterialRaisedButton(); - materialDivider2 = new MaterialSkin.Controls.MaterialDivider(); btnResume = new MaterialSkin.Controls.MaterialRaisedButton(); + btnHotReload = new MaterialSkin.Controls.MaterialRaisedButton(); + materialDivider2 = new MaterialSkin.Controls.MaterialDivider(); ((System.ComponentModel.ISupportInitialize)previewWindow1).BeginInit(); flowLayoutPanel1.SuspendLayout(); flowLayoutPanel2.SuspendLayout(); @@ -51,9 +52,9 @@ // lblCameraName.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; lblCameraName.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold, GraphicsUnit.Point, 204); - lblCameraName.Location = new Point(8, 8); + lblCameraName.Location = new Point(9, 11); lblCameraName.Name = "lblCameraName"; - lblCameraName.Size = new Size(744, 21); + lblCameraName.Size = new Size(850, 28); lblCameraName.TabIndex = 2; lblCameraName.Text = "CameraName"; lblCameraName.TextAlign = ContentAlignment.TopCenter; @@ -62,9 +63,9 @@ // label1.Anchor = AnchorStyles.Top | AnchorStyles.Right; label1.Font = new Font("Segoe UI", 12F); - label1.Location = new Point(784, 0); + label1.Location = new Point(896, 0); label1.Name = "label1"; - label1.Size = new Size(408, 21); + label1.Size = new Size(466, 28); label1.TabIndex = 4; label1.Text = "Errors"; label1.TextAlign = ContentAlignment.TopCenter; @@ -72,9 +73,10 @@ // previewWindow1 // previewWindow1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - previewWindow1.Location = new Point(0, 32); + previewWindow1.Location = new Point(0, 43); + previewWindow1.Margin = new Padding(3, 4, 3, 4); previewWindow1.Name = "previewWindow1"; - previewWindow1.Size = new Size(752, 456); + previewWindow1.Size = new Size(859, 608); previewWindow1.SizeMode = PictureBoxSizeMode.Zoom; previewWindow1.TabIndex = 5; previewWindow1.TabStop = false; @@ -84,28 +86,31 @@ materialDivider1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; materialDivider1.BackColor = Color.FromArgb(55, 71, 79); materialDivider1.Depth = 0; - materialDivider1.Location = new Point(760, 24); + materialDivider1.Location = new Point(869, 32); + materialDivider1.Margin = new Padding(3, 4, 3, 4); materialDivider1.MouseState = MaterialSkin.MouseState.HOVER; materialDivider1.Name = "materialDivider1"; - materialDivider1.Size = new Size(1, 456); + materialDivider1.Size = new Size(1, 608); materialDivider1.TabIndex = 6; materialDivider1.Text = "materialDivider1"; // // errorPreview1 // errorPreview1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; - errorPreview1.Location = new Point(776, 24); + errorPreview1.Location = new Point(887, 32); + errorPreview1.Margin = new Padding(3, 5, 3, 5); errorPreview1.Name = "errorPreview1"; - errorPreview1.Size = new Size(416, 456); + errorPreview1.Size = new Size(475, 608); errorPreview1.TabIndex = 7; // // stats1 // stats1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; stats1.BackColor = Color.White; - stats1.Location = new Point(1200, 32); + stats1.Location = new Point(1371, 43); + stats1.Margin = new Padding(3, 5, 3, 5); stats1.Name = "stats1"; - stats1.Size = new Size(232, 456); + stats1.Size = new Size(265, 608); stats1.TabIndex = 8; // // btnStart @@ -114,13 +119,14 @@ btnStart.AutoSizeMode = AutoSizeMode.GrowAndShrink; btnStart.Depth = 0; btnStart.DrawBorder = false; - btnStart.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + btnStart.Font = new Font("Segoe UI", 9F, FontStyle.Bold, GraphicsUnit.Point, 0); btnStart.Icon = null; - btnStart.Location = new Point(21, 73); + btnStart.Location = new Point(25, 97); + btnStart.Margin = new Padding(3, 4, 3, 4); btnStart.MouseState = MaterialSkin.MouseState.HOVER; btnStart.Name = "btnStart"; btnStart.Primary = true; - btnStart.Size = new Size(176, 64); + btnStart.Size = new Size(201, 85); btnStart.TabIndex = 9; btnStart.Text = "Start"; btnStart.UseVisualStyleBackColor = true; @@ -130,11 +136,12 @@ flowLayoutPanel1.Controls.Add(flowLayoutPanel2); flowLayoutPanel1.Dock = DockStyle.Right; flowLayoutPanel1.FlowDirection = FlowDirection.TopDown; - flowLayoutPanel1.Location = new Point(1448, 0); + flowLayoutPanel1.Location = new Point(1655, 0); + flowLayoutPanel1.Margin = new Padding(3, 4, 3, 4); flowLayoutPanel1.Name = "flowLayoutPanel1"; - flowLayoutPanel1.Padding = new Padding(8, 24, 8, 8); + flowLayoutPanel1.Padding = new Padding(9, 32, 9, 11); flowLayoutPanel1.RightToLeft = RightToLeft.Yes; - flowLayoutPanel1.Size = new Size(240, 498); + flowLayoutPanel1.Size = new Size(274, 664); flowLayoutPanel1.TabIndex = 10; // // flowLayoutPanel2 @@ -144,9 +151,11 @@ flowLayoutPanel2.Controls.Add(btnStop); flowLayoutPanel2.Controls.Add(btnPause); flowLayoutPanel2.Controls.Add(btnResume); - flowLayoutPanel2.Location = new Point(21, 27); + flowLayoutPanel2.Controls.Add(btnHotReload); + flowLayoutPanel2.Location = new Point(24, 36); + flowLayoutPanel2.Margin = new Padding(3, 4, 3, 4); flowLayoutPanel2.Name = "flowLayoutPanel2"; - flowLayoutPanel2.Size = new Size(200, 365); + flowLayoutPanel2.Size = new Size(229, 578); flowLayoutPanel2.TabIndex = 12; // // btnSelectRecipe @@ -157,11 +166,12 @@ btnSelectRecipe.DrawBorder = true; btnSelectRecipe.Font = new Font("Segoe UI", 9F, FontStyle.Bold); btnSelectRecipe.Icon = null; - btnSelectRecipe.Location = new Point(21, 3); + btnSelectRecipe.Location = new Point(25, 4); + btnSelectRecipe.Margin = new Padding(3, 4, 3, 4); btnSelectRecipe.MouseState = MaterialSkin.MouseState.HOVER; btnSelectRecipe.Name = "btnSelectRecipe"; btnSelectRecipe.Primary = false; - btnSelectRecipe.Size = new Size(176, 64); + btnSelectRecipe.Size = new Size(201, 85); btnSelectRecipe.TabIndex = 11; btnSelectRecipe.Text = "Select recipe"; btnSelectRecipe.UseVisualStyleBackColor = true; @@ -174,11 +184,12 @@ btnStop.DrawBorder = false; btnStop.Font = new Font("Segoe UI", 9F, FontStyle.Bold); btnStop.Icon = null; - btnStop.Location = new Point(21, 143); + btnStop.Location = new Point(25, 190); + btnStop.Margin = new Padding(3, 4, 3, 4); btnStop.MouseState = MaterialSkin.MouseState.HOVER; btnStop.Name = "btnStop"; btnStop.Primary = true; - btnStop.Size = new Size(176, 64); + btnStop.Size = new Size(201, 85); btnStop.TabIndex = 10; btnStop.Text = "Stop"; btnStop.UseVisualStyleBackColor = true; @@ -191,27 +202,16 @@ btnPause.DrawBorder = true; btnPause.Font = new Font("Segoe UI", 9F, FontStyle.Bold); btnPause.Icon = null; - btnPause.Location = new Point(21, 213); + btnPause.Location = new Point(25, 283); + btnPause.Margin = new Padding(3, 4, 3, 4); btnPause.MouseState = MaterialSkin.MouseState.HOVER; btnPause.Name = "btnPause"; btnPause.Primary = false; - btnPause.Size = new Size(176, 64); + btnPause.Size = new Size(201, 85); btnPause.TabIndex = 12; btnPause.Text = "Pause"; btnPause.UseVisualStyleBackColor = true; // - // materialDivider2 - // - materialDivider2.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; - materialDivider2.BackColor = Color.FromArgb(55, 71, 79); - materialDivider2.Depth = 0; - materialDivider2.Location = new Point(1192, 24); - materialDivider2.MouseState = MaterialSkin.MouseState.HOVER; - materialDivider2.Name = "materialDivider2"; - materialDivider2.Size = new Size(1, 456); - materialDivider2.TabIndex = 11; - materialDivider2.Text = "materialDivider2"; - // // btnResume // btnResume.Anchor = AnchorStyles.Top | AnchorStyles.Right; @@ -220,18 +220,51 @@ btnResume.DrawBorder = true; btnResume.Font = new Font("Segoe UI", 9F, FontStyle.Bold); btnResume.Icon = null; - btnResume.Location = new Point(21, 283); + btnResume.Location = new Point(25, 376); + btnResume.Margin = new Padding(3, 4, 3, 4); btnResume.MouseState = MaterialSkin.MouseState.HOVER; btnResume.Name = "btnResume"; btnResume.Primary = false; - btnResume.Size = new Size(176, 64); + btnResume.Size = new Size(201, 85); btnResume.TabIndex = 13; btnResume.Text = "Resume"; btnResume.UseVisualStyleBackColor = true; // + // btnHotReload + // + btnHotReload.Anchor = AnchorStyles.Top | AnchorStyles.Right; + btnHotReload.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnHotReload.Depth = 0; + btnHotReload.DrawBorder = true; + btnHotReload.Font = new Font("Segoe UI", 9F, FontStyle.Bold); + btnHotReload.Icon = null; + btnHotReload.Location = new Point(25, 469); + btnHotReload.Margin = new Padding(3, 4, 3, 4); + btnHotReload.MouseState = MaterialSkin.MouseState.HOVER; + btnHotReload.Name = "btnHotReload"; + btnHotReload.Primary = false; + btnHotReload.Size = new Size(201, 85); + btnHotReload.TabIndex = 14; + btnHotReload.Text = "Hot Reload"; + btnHotReload.UseVisualStyleBackColor = true; + btnHotReload.Visible = false; + // + // materialDivider2 + // + materialDivider2.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; + materialDivider2.BackColor = Color.FromArgb(55, 71, 79); + materialDivider2.Depth = 0; + materialDivider2.Location = new Point(1362, 32); + materialDivider2.Margin = new Padding(3, 4, 3, 4); + materialDivider2.MouseState = MaterialSkin.MouseState.HOVER; + materialDivider2.Name = "materialDivider2"; + materialDivider2.Size = new Size(1, 608); + materialDivider2.TabIndex = 11; + materialDivider2.Text = "materialDivider2"; + // // SingleCameraControl // - AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleDimensions = new SizeF(8F, 20F); AutoScaleMode = AutoScaleMode.Font; BackColor = Color.White; Controls.Add(materialDivider2); @@ -242,8 +275,9 @@ Controls.Add(previewWindow1); Controls.Add(label1); Controls.Add(lblCameraName); + Margin = new Padding(3, 4, 3, 4); Name = "SingleCameraControl"; - Size = new Size(1688, 498); + Size = new Size(1929, 664); ((System.ComponentModel.ISupportInitialize)previewWindow1).EndInit(); flowLayoutPanel1.ResumeLayout(false); flowLayoutPanel2.ResumeLayout(false); @@ -266,5 +300,6 @@ private FlowLayoutPanel flowLayoutPanel2; private MaterialSkin.Controls.MaterialRaisedButton btnPause; private MaterialSkin.Controls.MaterialRaisedButton btnResume; + private MaterialSkin.Controls.MaterialRaisedButton btnHotReload; } } diff --git a/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs b/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs index 0c3fa93..02f8590 100644 --- a/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs +++ b/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs @@ -47,7 +47,9 @@ namespace VisionBuilder.UI.Windows.Components btnSelectRecipe.DataBindings.Add(nameof(btnSelectRecipe.Primary), _singleCameraVm, nameof(_singleCameraVm.RecipeNotSelected), true, DataSourceUpdateMode.OnPropertyChanged); btnPause.DataBindings.Add(nameof(btnPause.Visible), _singleCameraVm, nameof(_singleCameraVm.CanPause), true, DataSourceUpdateMode.OnPropertyChanged); btnResume.DataBindings.Add(nameof(btnResume.Visible), _singleCameraVm, nameof(_singleCameraVm.CanResume), true, DataSourceUpdateMode.OnPropertyChanged); - + + btnHotReload.Command = _singleCameraVm.HotReloadCommand; + btnHotReload.DataBindings.Add(nameof(btnHotReload.Visible), _singleCameraVm, nameof(_singleCameraVm.CanHotReload), true, DataSourceUpdateMode.OnPropertyChanged); }