hot reload

This commit is contained in:
EugeneTes
2026-02-18 11:01:35 +01:00
parent 2bac7140a4
commit 14fe822962
9 changed files with 345 additions and 47 deletions

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(ls:*)"
]
}
}

94
CLAUDE.md Normal file
View File

@@ -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 `<PackageReference>` 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<TEvent>` in `VisionBuilder.UI.Common/Commands/`. Key events: `SessionStartedEvent`, `SessionEndedEvent`, `ImageProcessedEvent`, `ErrorsInSequenceEvent`. `IRecognitionControl` surfaces these as `Action<T>` 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.

View File

@@ -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<Dictionary<string, object>>(jsonString);
if (workflowData == null) return;
// Update Configuration
if (workflowData.ContainsKey("Configuration"))
{
var configElement = (JsonElement)workflowData["Configuration"];
var configData = JsonSerializer.Deserialize<Dictionary<string, object>>(configElement.GetRawText());
if (configData != null)
{
var convertedConfigData = ConvertJsonElementsToNatives(configData);
if (convertedConfigData.ContainsKey("RuntimeCameraType"))
Configuration.RuntimeCameraType = Enum.Parse<ECameraType>(convertedConfigData["RuntimeCameraType"].ToString());
if (convertedConfigData.ContainsKey("DevelopmentCameraType"))
Configuration.DevelopmentCameraType = Enum.Parse<ECameraType>(convertedConfigData["DevelopmentCameraType"].ToString());
if (convertedConfigData.ContainsKey("Outputs"))
Configuration.Outputs = Enum.Parse<EOutputs>(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<List<Dictionary<string, object>>>(operationsElement.GetRawText());
if (operationsArray != null)
{
foreach (var operationData in operationsArray)
{
if (!operationData.ContainsKey("Data")) continue;
var dataElement = (JsonElement)operationData["Data"];
var operationDict = JsonSerializer.Deserialize<Dictionary<string, object>>(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);
}
}
}
}
}
}

View File

@@ -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<SessionStartedEvent> SessionStarted = delegate { };
public event Action<SessionEndedEvent> SessionEnded = delegate { };
public event Action<ErrorsInSequenceEvent> ErrorsInSequenceAlarm = delegate { };
public event Action RecipeFileChanged = delegate { };
public virtual void HotReload() { }
protected void RaiseRecipeFileChanged() => RecipeFileChanged?.Invoke();
protected virtual void Cleanup() { }
}

View File

@@ -17,4 +17,6 @@ public interface IRecognitionControl
event Action<SessionEndedEvent> SessionEnded;
event Action<ErrorsInSequenceEvent> ErrorsInSequenceAlarm;
void HotReload();
event Action RecipeFileChanged;
}

View File

@@ -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]

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}