plugin system docs
custom buttons for plugins calibration functions for Leeform
@@ -68,6 +68,8 @@ Plugins implement `IPlugin` with two registration points:
|
|||||||
|
|
||||||
Modules implement `IVisionBuilderModule.InitializeModule()` for deferred initialization.
|
Modules implement `IVisionBuilderModule.InitializeModule()` for deferred initialization.
|
||||||
|
|
||||||
|
For detailed plugin architecture documentation and step-by-step implementation guide, see [`docs/PLUGIN_SYSTEM.md`](docs/PLUGIN_SYSTEM.md).
|
||||||
|
|
||||||
### DI Container
|
### 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Inspectron.Settings;
|
||||||
|
using VisionBuilder.UI.Common;
|
||||||
|
|
||||||
|
namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera;
|
||||||
|
|
||||||
|
public class CaptureSettings : ISettings
|
||||||
|
{
|
||||||
|
public string CameraName { get; }
|
||||||
|
|
||||||
|
public CaptureSettings(string cameraName)
|
||||||
|
{
|
||||||
|
CameraName = cameraName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Width { get; set; } = 2028;
|
||||||
|
public int Height { get; set; } = 1520;
|
||||||
|
public int ShutterSpeed { get; set; } = 10000;
|
||||||
|
public int Framerate { get; set; } = 2;
|
||||||
|
public double AwbGainRed { get; set; } = 3.86;
|
||||||
|
public double AwbGainBlue { get; set; } = 1.46;
|
||||||
|
|
||||||
|
public string BuildArguments()
|
||||||
|
{
|
||||||
|
return $"--codec mjpeg -t0 --width {Width} --height {Height} " +
|
||||||
|
$"--shutter {ShutterSpeed} --framerate {Framerate} " +
|
||||||
|
$"--awbgains {AwbGainRed:F2},{AwbGainBlue:F2} --nopreview -o -";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSettings(InspectronSettings settings)
|
||||||
|
{
|
||||||
|
settings.RegisterSimple(this, () => Width, CameraName + "/Sources/Libcamera", nameof(Width));
|
||||||
|
settings.RegisterSimple(this, () => Height, CameraName + "/Sources/Libcamera", nameof(Height));
|
||||||
|
settings.RegisterSimple(this, () => ShutterSpeed, CameraName + "/Sources/Libcamera", nameof(ShutterSpeed));
|
||||||
|
settings.RegisterSimple(this, () => Framerate, CameraName + "/Sources/Libcamera", nameof(Framerate));
|
||||||
|
settings.RegisterSimple(this, () => AwbGainRed, CameraName + "/Sources/Libcamera", nameof(AwbGainRed));
|
||||||
|
settings.RegisterSimple(this, () => AwbGainBlue, CameraName + "/Sources/Libcamera", nameof(AwbGainBlue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,12 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera
|
|||||||
{
|
{
|
||||||
public class LibcameraImageSource : IImageSource, IVisionBuilderModule
|
public class LibcameraImageSource : IImageSource, IVisionBuilderModule
|
||||||
{
|
{
|
||||||
|
private CaptureSettings _settings;
|
||||||
|
|
||||||
|
public LibcameraImageSource(CaptureSettings settings)
|
||||||
|
{
|
||||||
|
_settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Mat> GetImage(CancellationToken token)
|
public async Task<Mat> GetImage(CancellationToken token)
|
||||||
{
|
{
|
||||||
@@ -25,15 +29,6 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera
|
|||||||
byte[] JpegFooter = new byte[] { 0xff, 0xd9 };
|
byte[] JpegFooter = new byte[] { 0xff, 0xd9 };
|
||||||
int ChunkSize = 1024;
|
int ChunkSize = 1024;
|
||||||
|
|
||||||
ProcessStartInfo psi = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "libcamera-vid",
|
|
||||||
Arguments = "--codec mjpeg -t0 --width 2028 --height 1520 --shutter 10000 --framerate 16 --awbgains 3.86,1.46 --nopreview -o -",
|
|
||||||
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
UseShellExecute = false
|
|
||||||
};
|
|
||||||
|
|
||||||
Channel<Mat> _imageChannel = Channel.CreateBounded<Mat>(new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest });
|
Channel<Mat> _imageChannel = Channel.CreateBounded<Mat>(new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest });
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
@@ -46,6 +41,14 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera
|
|||||||
|
|
||||||
private void StartLoop()
|
private void StartLoop()
|
||||||
{
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "rpicam-vid",
|
||||||
|
Arguments = _settings.BuildArguments(),
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
|
||||||
using (Process process = Process.Start(psi))
|
using (Process process = Process.Start(psi))
|
||||||
using (BinaryReader br = new BinaryReader(process.StandardOutput.BaseStream))
|
using (BinaryReader br = new BinaryReader(process.StandardOutput.BaseStream))
|
||||||
{
|
{
|
||||||
|
|||||||
61
Plugins/LindtLeerformPlugin/LeerformModule.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using LindtLeerformPlugin.ViewModels;
|
||||||
|
using LindtLeerformPlugin.Views;
|
||||||
|
using Serilog;
|
||||||
|
using VisionBuilder.UI.Common;
|
||||||
|
using VisionBuilder.UI.Common.Processing;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
|
||||||
|
namespace LindtLeerformPlugin;
|
||||||
|
|
||||||
|
public class LeerformModule : IVisionBuilderModule
|
||||||
|
{
|
||||||
|
private readonly SingleCameraVM _singleCameraVm;
|
||||||
|
private readonly LeerformSettings _settings;
|
||||||
|
private readonly IImageSource _imageSource;
|
||||||
|
|
||||||
|
public LeerformModule(SingleCameraVM singleCameraVm, LeerformSettings settings, IImageSource imageSource)
|
||||||
|
{
|
||||||
|
_singleCameraVm = singleCameraVm;
|
||||||
|
_settings = settings;
|
||||||
|
_imageSource = imageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeModule()
|
||||||
|
{
|
||||||
|
var calibrateButton = new ButtonDefinition("leerform_calibrate", "Calibrate", OnCalibrateClicked);
|
||||||
|
_singleCameraVm.AddButton(calibrateButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCalibrateClicked()
|
||||||
|
{
|
||||||
|
_ = ShowCalibrationWindowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ShowCalibrationWindowAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
Log.Warning("Avalonia desktop lifetime not available, cannot show calibration window.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = desktop.MainWindow;
|
||||||
|
var vm = new CalibrationWindowViewModel(_imageSource, _settings);
|
||||||
|
var window = new CalibrationWindow { DataContext = vm };
|
||||||
|
|
||||||
|
if (parent != null)
|
||||||
|
await window.ShowDialog(parent);
|
||||||
|
else
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error showing calibration window");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
Plugins/LindtLeerformPlugin/LeerformSettings.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Inspectron.Settings;
|
||||||
|
using LindtLeerformPlugin.Models;
|
||||||
|
using VisionBuilder.UI.Common;
|
||||||
|
|
||||||
|
namespace LindtLeerformPlugin;
|
||||||
|
|
||||||
|
public class LeerformSettings : ISettings
|
||||||
|
{
|
||||||
|
public string CameraName { get; }
|
||||||
|
|
||||||
|
public LeerformSettings(string cameraName)
|
||||||
|
{
|
||||||
|
CameraName = cameraName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CalibrationData? CalibrationData { get; set; }
|
||||||
|
public int CheckerboardRows { get; set; } = 6;
|
||||||
|
public int CheckerboardCols { get; set; } = 9;
|
||||||
|
public string CalibrationImageDirectory { get; set; } = "CalibrationImages";
|
||||||
|
|
||||||
|
public void RegisterSettings(InspectronSettings settings)
|
||||||
|
{
|
||||||
|
settings.RegisterSimple(this, () => CalibrationData!, $"{CameraName}/Leerform", nameof(CalibrationData));
|
||||||
|
settings.RegisterSimple(this, () => CheckerboardRows, $"{CameraName}/Leerform", nameof(CheckerboardRows));
|
||||||
|
settings.RegisterSimple(this, () => CheckerboardCols, $"{CameraName}/Leerform", nameof(CheckerboardCols));
|
||||||
|
settings.RegisterSimple(this, () => CalibrationImageDirectory, $"{CameraName}/Leerform", nameof(CalibrationImageDirectory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CalibrationDataConverter : ITypeConverter
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
public object ConvertFrom(object value)
|
||||||
|
{
|
||||||
|
if (value is string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json) || json == "null")
|
||||||
|
return null!;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<CalibrationData>(json, JsonOptions)
|
||||||
|
?? throw new InvalidOperationException("Failed to deserialize CalibrationData from JSON.");
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Value must be a JSON string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertTo(object value, Type destinationType)
|
||||||
|
{
|
||||||
|
if (value is CalibrationData data)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(data, JsonOptions);
|
||||||
|
}
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Value must be a CalibrationData.");
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Plugins/LindtLeerformPlugin/LindtLeerformPlugin.csproj
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
|
<OutDir>..\..\VisionBuilder.UI.Avalonia.Uno\bin\Debug\Data\Plugins\LindtLeerformPlugin</OutDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj">
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime</ExcludeAssets>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\..\framework\Inspectron.Settings\Inspectron.Settings.csproj">
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime</ExcludeAssets>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.2.3">
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime</ExcludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.3">
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime</ExcludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3">
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime</ExcludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0">
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime</ExcludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108">
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime</ExcludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
10
Plugins/LindtLeerformPlugin/Models/CalibrationData.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace LindtLeerformPlugin.Models;
|
||||||
|
|
||||||
|
public class CalibrationData
|
||||||
|
{
|
||||||
|
public double[] CameraMatrix { get; set; } = [];
|
||||||
|
public double[] DistCoeffs { get; set; } = [];
|
||||||
|
public double RmsError { get; set; }
|
||||||
|
public int ImageWidth { get; set; }
|
||||||
|
public int ImageHeight { get; set; }
|
||||||
|
}
|
||||||
21
Plugins/LindtLeerformPlugin/Plugin.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Inspectron.Settings;
|
||||||
|
using LindtLeerformPlugin.Models;
|
||||||
|
using Ninject;
|
||||||
|
using VisionBuilder.UI.Common;
|
||||||
|
using VisionBuilder.UI.Common.Plugins;
|
||||||
|
|
||||||
|
namespace LindtLeerformPlugin;
|
||||||
|
|
||||||
|
public class Plugin : IPlugin
|
||||||
|
{
|
||||||
|
public void RegisterGlobalModules(IKernel kernel)
|
||||||
|
{
|
||||||
|
TypeConverterRegistry.Register<CalibrationData>(new CalibrationDataConverter());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterCameraModules(IKernel kernel, string cameraName)
|
||||||
|
{
|
||||||
|
kernel.Bind<LeerformSettings, ISettings>().ToConstant(new LeerformSettings(cameraName));
|
||||||
|
kernel.RegisterModule<LeerformModule>();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Plugins/LindtLeerformPlugin/Services/CameraCalibrationService.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using LindtLeerformPlugin.Models;
|
||||||
|
using OpenCvSharp;
|
||||||
|
|
||||||
|
namespace LindtLeerformPlugin.Services;
|
||||||
|
|
||||||
|
public static class CameraCalibrationService
|
||||||
|
{
|
||||||
|
public static CalibrationData Calibrate(string imageDirectory, Size patternSize)
|
||||||
|
{
|
||||||
|
var imageFiles = Directory.GetFiles(imageDirectory, "*.png");
|
||||||
|
if (imageFiles.Length == 0)
|
||||||
|
throw new InvalidOperationException($"No PNG images found in {imageDirectory}");
|
||||||
|
|
||||||
|
var objectPointsList = new List<Mat>();
|
||||||
|
var imagePointsList = new List<Mat>();
|
||||||
|
Size imageSize = default;
|
||||||
|
|
||||||
|
int cornerCount = patternSize.Width * patternSize.Height;
|
||||||
|
var objPts = new Point3f[cornerCount];
|
||||||
|
for (int row = 0; row < patternSize.Height; row++)
|
||||||
|
for (int col = 0; col < patternSize.Width; col++)
|
||||||
|
objPts[row * patternSize.Width + col] = new Point3f(col, row, 0);
|
||||||
|
|
||||||
|
int found = 0;
|
||||||
|
foreach (var file in imageFiles)
|
||||||
|
{
|
||||||
|
using var image = Cv2.ImRead(file);
|
||||||
|
using var gray = new Mat();
|
||||||
|
Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
|
||||||
|
imageSize = new Size(image.Width, image.Height);
|
||||||
|
|
||||||
|
if (Cv2.FindChessboardCorners(gray, patternSize, out var corners,
|
||||||
|
ChessboardFlags.AdaptiveThresh | ChessboardFlags.FastCheck))
|
||||||
|
{
|
||||||
|
Cv2.CornerSubPix(gray, corners,
|
||||||
|
new Size(11, 11), new Size(-1, -1),
|
||||||
|
new TermCriteria(CriteriaTypes.Eps | CriteriaTypes.MaxIter, 30, 0.001));
|
||||||
|
|
||||||
|
var objMat = new Mat(cornerCount, 1, MatType.CV_32FC3);
|
||||||
|
for (int i = 0; i < cornerCount; i++)
|
||||||
|
objMat.Set(i, 0, objPts[i]);
|
||||||
|
objectPointsList.Add(objMat);
|
||||||
|
|
||||||
|
var imgMat = new Mat(corners.Length, 1, MatType.CV_32FC2);
|
||||||
|
for (int i = 0; i < corners.Length; i++)
|
||||||
|
imgMat.Set(i, 0, corners[i]);
|
||||||
|
imagePointsList.Add(imgMat);
|
||||||
|
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found == 0)
|
||||||
|
throw new InvalidOperationException("Checkerboard corners not found in any image");
|
||||||
|
|
||||||
|
var cameraMatrix = new Mat();
|
||||||
|
var distCoeffs = new Mat();
|
||||||
|
double rms = Cv2.CalibrateCamera(
|
||||||
|
objectPointsList, imagePointsList, imageSize,
|
||||||
|
cameraMatrix, distCoeffs,
|
||||||
|
out _, out _);
|
||||||
|
|
||||||
|
var data = new CalibrationData
|
||||||
|
{
|
||||||
|
CameraMatrix = MatToArray(cameraMatrix),
|
||||||
|
DistCoeffs = MatToArray(distCoeffs),
|
||||||
|
RmsError = rms,
|
||||||
|
ImageWidth = imageSize.Width,
|
||||||
|
ImageHeight = imageSize.Height
|
||||||
|
};
|
||||||
|
|
||||||
|
cameraMatrix.Dispose();
|
||||||
|
distCoeffs.Dispose();
|
||||||
|
foreach (var m in objectPointsList) m.Dispose();
|
||||||
|
foreach (var m in imagePointsList) m.Dispose();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (Mat cameraMatrix, Mat distCoeffs) LoadCalibrationMats(CalibrationData data)
|
||||||
|
{
|
||||||
|
var cameraMatrix = new Mat(3, 3, MatType.CV_64F);
|
||||||
|
for (int i = 0; i < 9; i++)
|
||||||
|
cameraMatrix.Set(i / 3, i % 3, data.CameraMatrix[i]);
|
||||||
|
|
||||||
|
var distCoeffs = new Mat(1, data.DistCoeffs.Length, MatType.CV_64F);
|
||||||
|
for (int i = 0; i < data.DistCoeffs.Length; i++)
|
||||||
|
distCoeffs.Set(0, i, data.DistCoeffs[i]);
|
||||||
|
|
||||||
|
return (cameraMatrix, distCoeffs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double[] MatToArray(Mat mat)
|
||||||
|
{
|
||||||
|
var total = mat.Rows * mat.Cols;
|
||||||
|
var arr = new double[total];
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
arr[i] = mat.At<double>(i / mat.Cols, i % mat.Cols);
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Plugins/LindtLeerformPlugin/Services/ImageConverter.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using OpenCvSharp;
|
||||||
|
|
||||||
|
namespace LindtLeerformPlugin.Services;
|
||||||
|
|
||||||
|
public static class ImageConverter
|
||||||
|
{
|
||||||
|
public static Avalonia.Media.Imaging.Bitmap MatToAvaloniaBitmap(Mat mat)
|
||||||
|
{
|
||||||
|
var encoded = mat.ImEncode(".bmp");
|
||||||
|
using var ms = new MemoryStream(encoded);
|
||||||
|
return new Avalonia.Media.Imaging.Bitmap(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
using Avalonia.Threading;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LindtLeerformPlugin.Models;
|
||||||
|
using LindtLeerformPlugin.Services;
|
||||||
|
using OpenCvSharp;
|
||||||
|
using VisionBuilder.UI.Common.Processing;
|
||||||
|
|
||||||
|
namespace LindtLeerformPlugin.ViewModels;
|
||||||
|
|
||||||
|
public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IImageSource _imageSource;
|
||||||
|
private readonly LeerformSettings _settings;
|
||||||
|
private CancellationTokenSource? _previewCts;
|
||||||
|
private Mat? _lastFrame;
|
||||||
|
|
||||||
|
[ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage;
|
||||||
|
[ObservableProperty] private string _statusText = "Ready";
|
||||||
|
[ObservableProperty] private int _capturedImageCount;
|
||||||
|
[ObservableProperty] private bool _isPreviewRunning;
|
||||||
|
[ObservableProperty] private bool _isCalibrated;
|
||||||
|
[ObservableProperty] private string _calibrationImageDirectory;
|
||||||
|
[ObservableProperty] private double _rmsError;
|
||||||
|
|
||||||
|
public CalibrationWindowViewModel(IImageSource imageSource, LeerformSettings settings)
|
||||||
|
{
|
||||||
|
_imageSource = imageSource;
|
||||||
|
_settings = settings;
|
||||||
|
|
||||||
|
_calibrationImageDirectory = settings.CalibrationImageDirectory;
|
||||||
|
_isCalibrated = settings.CalibrationData is { CameraMatrix.Length: > 0 };
|
||||||
|
_rmsError = settings.CalibrationData?.RmsError ?? 0;
|
||||||
|
|
||||||
|
UpdateCapturedImageCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCapturedImageCount()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(CalibrationImageDirectory))
|
||||||
|
CapturedImageCount = Directory.GetFiles(CalibrationImageDirectory, "*.png").Length;
|
||||||
|
else
|
||||||
|
CapturedImageCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void StartPreview()
|
||||||
|
{
|
||||||
|
if (IsPreviewRunning) return;
|
||||||
|
|
||||||
|
IsPreviewRunning = true;
|
||||||
|
_previewCts = new CancellationTokenSource();
|
||||||
|
_ = PreviewLoopAsync(_previewCts.Token);
|
||||||
|
StatusText = "Preview running";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void StopPreview()
|
||||||
|
{
|
||||||
|
if (!IsPreviewRunning) return;
|
||||||
|
|
||||||
|
_previewCts?.Cancel();
|
||||||
|
IsPreviewRunning = false;
|
||||||
|
StatusText = "Preview stopped";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CaptureImage()
|
||||||
|
{
|
||||||
|
if (_lastFrame == null || _lastFrame.Empty()) return;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(CalibrationImageDirectory);
|
||||||
|
var filename = $"calib_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png";
|
||||||
|
var path = Path.Combine(CalibrationImageDirectory, filename);
|
||||||
|
Cv2.ImWrite(path, _lastFrame);
|
||||||
|
UpdateCapturedImageCount();
|
||||||
|
StatusText = $"Captured: {filename}";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RunCalibration()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StatusText = "Calibrating...";
|
||||||
|
var patternSize = new Size(_settings.CheckerboardCols, _settings.CheckerboardRows);
|
||||||
|
var data = CameraCalibrationService.Calibrate(CalibrationImageDirectory, patternSize);
|
||||||
|
|
||||||
|
_settings.CalibrationData = data;
|
||||||
|
_settings.CalibrationImageDirectory = CalibrationImageDirectory;
|
||||||
|
|
||||||
|
IsCalibrated = true;
|
||||||
|
RmsError = data.RmsError;
|
||||||
|
StatusText = $"Calibrated (RMS: {data.RmsError:F4})";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusText = $"Calibration error: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ClearCalibrationImages()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(CalibrationImageDirectory)) return;
|
||||||
|
|
||||||
|
foreach (var file in Directory.GetFiles(CalibrationImageDirectory, "*.png"))
|
||||||
|
File.Delete(file);
|
||||||
|
|
||||||
|
UpdateCapturedImageCount();
|
||||||
|
StatusText = "Calibration images cleared";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PreviewLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var frame = await _imageSource.GetImage(ct);
|
||||||
|
|
||||||
|
_lastFrame?.Dispose();
|
||||||
|
_lastFrame = frame;
|
||||||
|
|
||||||
|
var bitmap = ImageConverter.MatToAvaloniaBitmap(frame);
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
var old = PreviewImage;
|
||||||
|
PreviewImage = bitmap;
|
||||||
|
old?.Dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
StatusText = $"Preview error: {ex.Message}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
StopPreview();
|
||||||
|
_lastFrame?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
67
Plugins/LindtLeerformPlugin/Views/CalibrationWindow.axaml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:LindtLeerformPlugin.ViewModels"
|
||||||
|
x:Class="LindtLeerformPlugin.Views.CalibrationWindow"
|
||||||
|
x:DataType="vm:CalibrationWindowViewModel"
|
||||||
|
Title="Leerform Calibration"
|
||||||
|
Width="1024" Height="800">
|
||||||
|
|
||||||
|
<DockPanel>
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="8">
|
||||||
|
<Button Content="Start Preview" Command="{Binding StartPreviewCommand}" IsEnabled="{Binding !IsPreviewRunning}" />
|
||||||
|
<Button Content="Stop Preview" Command="{Binding StopPreviewCommand}" IsEnabled="{Binding IsPreviewRunning}" />
|
||||||
|
<Button Content="Capture Image" Command="{Binding CaptureImageCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<Border DockPanel.Dock="Bottom" Background="#1E1E1E" Padding="8,4">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<TextBlock Text="{Binding StatusText}" Foreground="LightGray" />
|
||||||
|
<TextBlock Foreground="LightGray">
|
||||||
|
<TextBlock.Text>
|
||||||
|
<MultiBinding StringFormat="Captured: {0}">
|
||||||
|
<Binding Path="CapturedImageCount" />
|
||||||
|
</MultiBinding>
|
||||||
|
</TextBlock.Text>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Settings Panel -->
|
||||||
|
<Border DockPanel.Dock="Right" Width="220" Padding="12" Background="#2D2D2D">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Calibration Settings" FontWeight="Bold" FontSize="14" Foreground="White" />
|
||||||
|
|
||||||
|
<TextBlock Text="Image Directory" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding CalibrationImageDirectory}" />
|
||||||
|
|
||||||
|
<Separator Margin="0,8" />
|
||||||
|
|
||||||
|
<Button Content="Calibrate" Command="{Binding RunCalibrationCommand}" Margin="0,4"
|
||||||
|
Background="#00C853" Foreground="White" FontWeight="Bold" />
|
||||||
|
<Button Content="Clear Images" Command="{Binding ClearCalibrationImagesCommand}" Margin="0,4"
|
||||||
|
Background="#FF6D00" Foreground="White" FontWeight="Bold" />
|
||||||
|
|
||||||
|
<Separator Margin="0,8" />
|
||||||
|
|
||||||
|
<TextBlock Text="Calibrated" Foreground="LimeGreen" IsVisible="{Binding IsCalibrated}" />
|
||||||
|
<TextBlock Foreground="LightGray" IsVisible="{Binding IsCalibrated}">
|
||||||
|
<TextBlock.Text>
|
||||||
|
<MultiBinding StringFormat="RMS Error: {0:F4}">
|
||||||
|
<Binding Path="RmsError" />
|
||||||
|
</MultiBinding>
|
||||||
|
</TextBlock.Text>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Live Preview -->
|
||||||
|
<Border Background="#1A1A1A" Margin="4">
|
||||||
|
<Image Source="{Binding PreviewImage}" Stretch="Uniform" />
|
||||||
|
</Border>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
</Window>
|
||||||
18
Plugins/LindtLeerformPlugin/Views/CalibrationWindow.axaml.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using LindtLeerformPlugin.ViewModels;
|
||||||
|
|
||||||
|
namespace LindtLeerformPlugin.Views;
|
||||||
|
|
||||||
|
public partial class CalibrationWindow : Window
|
||||||
|
{
|
||||||
|
public CalibrationWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosing(WindowClosingEventArgs e)
|
||||||
|
{
|
||||||
|
(DataContext as CalibrationWindowViewModel)?.Dispose();
|
||||||
|
base.OnClosing(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
VisionBuilder.UI.Avalonia.Uno/App.axaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="VisionBuilder.UI.Avalonia.Uno.App">
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
89
VisionBuilder.UI.Avalonia.Uno/App.axaml.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Inspectron.Settings;
|
||||||
|
using Ninject;
|
||||||
|
using Ninject.Extensions.ChildKernel;
|
||||||
|
using VisionBuilder.UI.Common;
|
||||||
|
using VisionBuilder.UI.Common.Plugins;
|
||||||
|
using VisionBuilder.UI.Common.Processing;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.IOCommander;
|
||||||
|
using VisionBuilder.UI.Recipes.HawkeyeRecipe;
|
||||||
|
using VisionBuilder.UI.Ringbuffer;
|
||||||
|
using VisionBuilder.UI.Statistics;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
using Lindt.Colorballs.Duo.Utils;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno;
|
||||||
|
|
||||||
|
public class App : Application
|
||||||
|
{
|
||||||
|
private const string CAMERA1 = "Camera 1";
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
var args = desktop.Args ?? [];
|
||||||
|
var commandLineParser = new CommandLineParser(args);
|
||||||
|
|
||||||
|
var settings = new InspectronSettings("../Data/Config");
|
||||||
|
|
||||||
|
var mainKernel = Common.VisionBuilder.CreateMainKernel(settings);
|
||||||
|
|
||||||
|
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
||||||
|
mainKernel.UsePlugins(profile);
|
||||||
|
|
||||||
|
mainKernel
|
||||||
|
.UseIOCommander([CAMERA1])
|
||||||
|
.UseAvaloniaServices()
|
||||||
|
.RegisterGlobalPlugins();
|
||||||
|
|
||||||
|
mainKernel.Get<ILoadingService>().StartLoading("Camera initialization");
|
||||||
|
|
||||||
|
// CAMERA 1
|
||||||
|
ChildKernel kernelCamera1 = new ChildKernel(mainKernel);
|
||||||
|
|
||||||
|
kernelCamera1
|
||||||
|
.RegisterCamera(CAMERA1)
|
||||||
|
.UseStatistics(CAMERA1)
|
||||||
|
.UseRingbuffer(CAMERA1)
|
||||||
|
.UseHawkeyeRecipes(CAMERA1)
|
||||||
|
.UseCameraIOCommander(CAMERA1)
|
||||||
|
.RegisterCameraPlugins(CAMERA1);
|
||||||
|
|
||||||
|
// LOAD SETTINGS
|
||||||
|
mainKernel.RegisterSettings();
|
||||||
|
kernelCamera1.RegisterSettings();
|
||||||
|
settings.LoadSettings();
|
||||||
|
|
||||||
|
// Create camera and bind it (must be after settings load)
|
||||||
|
kernelCamera1.UseCamera();
|
||||||
|
|
||||||
|
// Initialize modules
|
||||||
|
mainKernel.InitializeModules();
|
||||||
|
kernelCamera1.InitializeModules();
|
||||||
|
|
||||||
|
mainKernel.Get<ILoadingService>().StopLoading("Camera initialization");
|
||||||
|
|
||||||
|
var mainWindowVm = mainKernel.Get<MainWindowVM>();
|
||||||
|
mainWindowVm.SingleCameraVms = [
|
||||||
|
kernelCamera1.Get<SingleCameraVM>()
|
||||||
|
];
|
||||||
|
|
||||||
|
var avaloniaUiSettings = mainKernel.Get<AvaloniaUISettings>();
|
||||||
|
|
||||||
|
settings.LoadSettings();
|
||||||
|
|
||||||
|
desktop.MainWindow = new MainWindow(mainWindowVm, avaloniaUiSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
VisionBuilder.UI.Avalonia.Uno/AvaloniaUISettings.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Inspectron.Settings;
|
||||||
|
using VisionBuilder.UI.Common;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno;
|
||||||
|
|
||||||
|
public class AvaloniaUISettings : ISettings
|
||||||
|
{
|
||||||
|
public bool FullScreen { get; set; } = false;
|
||||||
|
|
||||||
|
public void RegisterSettings(InspectronSettings settings)
|
||||||
|
{
|
||||||
|
settings.RegisterSimple(this, () => this.FullScreen, "System", nameof(FullScreen));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Converters;
|
||||||
|
|
||||||
|
public class BoolToVisibilityConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return value is true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using OpenCvSharp;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Converters;
|
||||||
|
|
||||||
|
public class MatToBitmapConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is Mat mat && !mat.Empty())
|
||||||
|
{
|
||||||
|
return ImageConverter.MatToAvaloniaBitmap(mat);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
22
VisionBuilder.UI.Avalonia.Uno/ModuleExtensions.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Ninject;
|
||||||
|
using VisionBuilder.UI.Common;
|
||||||
|
using VisionBuilder.UI.Common.Processing;
|
||||||
|
using VisionBuilder.UI.Common.Services;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno;
|
||||||
|
|
||||||
|
public static class ModuleExtensions
|
||||||
|
{
|
||||||
|
public static IKernel UseAvaloniaServices(this IKernel self)
|
||||||
|
{
|
||||||
|
self.Bind<ISettingsService>().To<AvaloniaSettingsService>().InSingletonScope();
|
||||||
|
self.Bind<ILoadingService>().To<AvaloniaLoadingService>().InSingletonScope();
|
||||||
|
self.Bind<IRecipeSelectionDialogService>().To<AvaloniaRecipeSelectionDialogService>().InSingletonScope();
|
||||||
|
self.Bind<IImagePreviewService>().To<AvaloniaImagePreviewService>().InSingletonScope();
|
||||||
|
self.Bind<IPasswordInputService>().To<AvaloniaPasswordInputService>().InSingletonScope();
|
||||||
|
self.Bind<AvaloniaUISettings, ISettings>().To<AvaloniaUISettings>().InSingletonScope();
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
VisionBuilder.UI.Avalonia.Uno/Program.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.LinuxFramebuffer;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Contains("--drm"))
|
||||||
|
BuildAvaloniaApp().StartLinuxDrm(args, card: null, scaling: 1.0);
|
||||||
|
else
|
||||||
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
=> AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.LogToTrace();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
|
||||||
|
public class AvaloniaImagePreviewService : IImagePreviewService
|
||||||
|
{
|
||||||
|
public async Task ShowImagePreviewAsync(ErrorPreviewVM errorPreviewVm)
|
||||||
|
{
|
||||||
|
var dialog = new ImagePreviewDialog(errorPreviewVm);
|
||||||
|
var parent = GetMainWindow();
|
||||||
|
if (parent != null)
|
||||||
|
await dialog.ShowDialog(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Window? GetMainWindow()
|
||||||
|
{
|
||||||
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
return desktop.MainWindow;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Serilog;
|
||||||
|
using VisionBuilder.UI.Common.Processing;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
|
||||||
|
public class AvaloniaLoadingService : ILoadingService
|
||||||
|
{
|
||||||
|
public void StartLoading(string title)
|
||||||
|
{
|
||||||
|
Log.Information("Loading: {Title}", title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopLoading(string title)
|
||||||
|
{
|
||||||
|
Log.Information("Loaded: {Title}", title);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using VisionBuilder.UI.Common.Services;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
|
||||||
|
public class AvaloniaPasswordInputService : IPasswordInputService
|
||||||
|
{
|
||||||
|
public async Task<string?> GetPasswordAsync()
|
||||||
|
{
|
||||||
|
var dialog = new PasswordDialog();
|
||||||
|
var parent = GetMainWindow();
|
||||||
|
if (parent != null)
|
||||||
|
return await dialog.ShowDialog<string?>(parent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Window? GetMainWindow()
|
||||||
|
{
|
||||||
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
return desktop.MainWindow;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
|
||||||
|
public class AvaloniaRecipeSelectionDialogService : IRecipeSelectionDialogService
|
||||||
|
{
|
||||||
|
private readonly IRecipeCreationTool _recipeCreationTool;
|
||||||
|
|
||||||
|
public AvaloniaRecipeSelectionDialogService(IRecipeCreationTool recipeCreationTool)
|
||||||
|
{
|
||||||
|
_recipeCreationTool = recipeCreationTool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SelectRecipeAsync(RecipeSelectionVM recipeSelectionVm)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
var dialog = new RecipeSelectionDialog(recipeSelectionVm, _recipeCreationTool, tcs);
|
||||||
|
var parent = GetMainWindow();
|
||||||
|
if (parent != null)
|
||||||
|
{
|
||||||
|
await dialog.ShowDialog(parent);
|
||||||
|
return tcs.Task.IsCompleted && tcs.Task.Result;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Window? GetMainWindow()
|
||||||
|
{
|
||||||
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
return desktop.MainWindow;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
|
||||||
|
public class AvaloniaSettingsService : ISettingsService
|
||||||
|
{
|
||||||
|
public Task ShowSettingsDialogAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
VisionBuilder.UI.Avalonia.Uno/Services/ImageConverter.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using OpenCvSharp;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
|
||||||
|
public static class ImageConverter
|
||||||
|
{
|
||||||
|
public static global::Avalonia.Media.Imaging.Bitmap MatToAvaloniaBitmap(Mat mat)
|
||||||
|
{
|
||||||
|
var encoded = mat.ImEncode(".bmp");
|
||||||
|
using var ms = new MemoryStream(encoded);
|
||||||
|
return new global::Avalonia.Media.Imaging.Bitmap(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
VisionBuilder.UI.Avalonia.Uno/Views/ImagePreviewDialog.axaml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.ImagePreviewDialog"
|
||||||
|
Title="Image Preview"
|
||||||
|
WindowState="Maximized"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="White"
|
||||||
|
CanResize="True">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*" Margin="16">
|
||||||
|
<!-- Row 0: Error name label -->
|
||||||
|
<TextBlock x:Name="LblImageName"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="16"
|
||||||
|
Margin="0,8,0,0" />
|
||||||
|
|
||||||
|
<!-- Row 1: Toolbar buttons -->
|
||||||
|
<Grid Grid.Row="1" Margin="0,8,0,8">
|
||||||
|
<!-- Left-aligned buttons -->
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Spacing="8">
|
||||||
|
<Button x:Name="BtnPrev"
|
||||||
|
Content="PREVIOUS IMAGE"
|
||||||
|
Width="168" Height="64"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
<Button x:Name="BtnSwitchView"
|
||||||
|
Content="SHOW ORIGINAL"
|
||||||
|
Width="168" Height="64"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Right-aligned buttons -->
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Spacing="8">
|
||||||
|
<Button x:Name="BtnLearn"
|
||||||
|
Content="LEARN THIS"
|
||||||
|
Width="168" Height="64"
|
||||||
|
IsVisible="False"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
<Button x:Name="BtnNext"
|
||||||
|
Content="NEXT IMAGE"
|
||||||
|
Width="168" Height="64"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
<Button x:Name="BtnClose"
|
||||||
|
Content="CLOSE"
|
||||||
|
Width="168" Height="64"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
Background="#D32F2F"
|
||||||
|
Foreground="White" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Row 2: Image -->
|
||||||
|
<Image x:Name="PreviewImage"
|
||||||
|
Grid.Row="2"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
|
||||||
|
public partial class ImagePreviewDialog : Window
|
||||||
|
{
|
||||||
|
private readonly ErrorPreviewVM _previewVm;
|
||||||
|
|
||||||
|
public ImagePreviewDialog(ErrorPreviewVM errorPreviewVm)
|
||||||
|
{
|
||||||
|
_previewVm = errorPreviewVm;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
_previewVm.PropertyChanged += PreviewVm_PropertyChanged;
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
UpdateImage();
|
||||||
|
BtnLearn.IsVisible = _previewVm.LearnButtonVisible;
|
||||||
|
BtnPrev.IsEnabled = _previewVm.IsPreviousImageEnabled;
|
||||||
|
BtnNext.IsEnabled = _previewVm.IsNextImageEnabled;
|
||||||
|
LblImageName.Text = _previewVm.ImageName;
|
||||||
|
|
||||||
|
// Wire button events (direct method calls like WinForms)
|
||||||
|
BtnPrev.Click += (_, _) => _previewVm.PreviousImage();
|
||||||
|
BtnNext.Click += (_, _) => _previewVm.NextImage();
|
||||||
|
BtnSwitchView.Click += (_, _) => _previewVm.SwitchView();
|
||||||
|
BtnLearn.Click += (_, _) => _previewVm.Learn();
|
||||||
|
BtnClose.Click += (_, _) => Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreviewVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
switch (e.PropertyName)
|
||||||
|
{
|
||||||
|
case nameof(ErrorPreviewVM.ImageAnalysis):
|
||||||
|
UpdateImage();
|
||||||
|
break;
|
||||||
|
case nameof(ErrorPreviewVM.LearnButtonVisible):
|
||||||
|
BtnLearn.IsVisible = _previewVm.LearnButtonVisible;
|
||||||
|
break;
|
||||||
|
case nameof(ErrorPreviewVM.IsPreviousImageEnabled):
|
||||||
|
BtnPrev.IsEnabled = _previewVm.IsPreviousImageEnabled;
|
||||||
|
break;
|
||||||
|
case nameof(ErrorPreviewVM.IsNextImageEnabled):
|
||||||
|
BtnNext.IsEnabled = _previewVm.IsNextImageEnabled;
|
||||||
|
break;
|
||||||
|
case nameof(ErrorPreviewVM.ImageName):
|
||||||
|
LblImageName.Text = _previewVm.ImageName;
|
||||||
|
break;
|
||||||
|
case nameof(ErrorPreviewVM.OriginalView):
|
||||||
|
BtnSwitchView.Content = _previewVm.OriginalView ? "SHOW ANALYSIS" : "SHOW ORIGINAL";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateImage()
|
||||||
|
{
|
||||||
|
if (_previewVm.ImageAnalysis != null && !_previewVm.ImageAnalysis.Empty())
|
||||||
|
{
|
||||||
|
PreviewImage.Source = ImageConverter.MatToAvaloniaBitmap(_previewVm.ImageAnalysis);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PreviewImage.Source = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
VisionBuilder.UI.Avalonia.Uno/Views/MainWindow.axaml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:views="using:VisionBuilder.UI.Avalonia.Uno.Views"
|
||||||
|
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.MainWindow"
|
||||||
|
Title="VisionBuilder UNO"
|
||||||
|
Width="1920" Height="1080"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Background="#FFFFFF">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,1,Auto">
|
||||||
|
<!-- Main camera view -->
|
||||||
|
<views:SingleCameraView x:Name="SingleCameraView" Grid.Row="0" />
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<Border Grid.Row="1" Background="#D32F2F" />
|
||||||
|
|
||||||
|
<!-- Bottom bar -->
|
||||||
|
<Grid Grid.Row="2" Height="70" Background="White">
|
||||||
|
<StackPanel x:Name="BottomBarPanel" Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="8">
|
||||||
|
<Button x:Name="BtnMinimize"
|
||||||
|
Content="MINIMIZE"
|
||||||
|
Width="176" Height="48"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
<Button x:Name="BtnExit"
|
||||||
|
Content="EXIT"
|
||||||
|
Width="176" Height="48"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock x:Name="LblVersion"
|
||||||
|
Text="v0.0.0"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Margin="0,0,8,4"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="#888888" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
197
VisionBuilder.UI.Avalonia.Uno/Views/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Reflection;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private readonly MainWindowVM _mainWindowVm;
|
||||||
|
private readonly AvaloniaUISettings _avaloniaUiSettings;
|
||||||
|
private readonly Dictionary<string, Control> _dynamicButtonControls = new();
|
||||||
|
|
||||||
|
public MainWindow(MainWindowVM mainWindowVm, AvaloniaUISettings avaloniaUiSettings)
|
||||||
|
{
|
||||||
|
_mainWindowVm = mainWindowVm;
|
||||||
|
_avaloniaUiSettings = avaloniaUiSettings;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
DataContext = _mainWindowVm;
|
||||||
|
|
||||||
|
SingleCameraView.SetViewModel(_mainWindowVm.SingleCameraVms[0]);
|
||||||
|
|
||||||
|
BtnMinimize.Click += BtnMinimize_Click;
|
||||||
|
BtnExit.Click += BtnExit_Click;
|
||||||
|
|
||||||
|
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
|
LblVersion.Text = version != null ? $"v{version.Major}.{version.Minor}.{version.Build}" : "v0.0.0";
|
||||||
|
|
||||||
|
_mainWindowVm.PropertyChanged += MainWindowVm_PropertyChanged;
|
||||||
|
|
||||||
|
_mainWindowVm.DynamicButtons.CollectionChanged += DynamicButtons_CollectionChanged;
|
||||||
|
foreach (var btn in _mainWindowVm.DynamicButtons)
|
||||||
|
AddDynamicButton(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
_mainWindowVm.OnProgramStarted();
|
||||||
|
|
||||||
|
if (_avaloniaUiSettings.FullScreen)
|
||||||
|
{
|
||||||
|
WindowState = WindowState.Maximized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosed(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnClosed(e);
|
||||||
|
_mainWindowVm.OnProgramClosed();
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MainWindowVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(_mainWindowVm.TestMode))
|
||||||
|
{
|
||||||
|
// Toggle accent color for test mode visual indication
|
||||||
|
// Green background when test mode is active
|
||||||
|
if (_mainWindowVm.TestMode)
|
||||||
|
{
|
||||||
|
Background = new global::Avalonia.Media.SolidColorBrush(
|
||||||
|
global::Avalonia.Media.Color.FromRgb(0x2E, 0x7D, 0x32)); // Green800
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Background = new global::Avalonia.Media.SolidColorBrush(
|
||||||
|
global::Avalonia.Media.Colors.White);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnMinimize_Click(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
WindowState = WindowState.Minimized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnExit_Click(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_mainWindowVm.OnProgramClosed();
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DynamicButtons_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (ButtonDefinition btn in e.NewItems!)
|
||||||
|
AddDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
foreach (ButtonDefinition btn in e.OldItems!)
|
||||||
|
RemoveDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Reset:
|
||||||
|
foreach (var ctrl in _dynamicButtonControls.Values)
|
||||||
|
BottomBarPanel.Children.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
Control control;
|
||||||
|
if (buttonDef.Type == EButtonType.Toggle)
|
||||||
|
{
|
||||||
|
var toggleBtn = new ToggleButton
|
||||||
|
{
|
||||||
|
Content = buttonDef.Title.ToUpperInvariant(),
|
||||||
|
Width = 176,
|
||||||
|
Height = 48,
|
||||||
|
FontSize = 14,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
|
||||||
|
IsChecked = buttonDef.IsToggled,
|
||||||
|
IsVisible = buttonDef.IsVisible,
|
||||||
|
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)),
|
||||||
|
BorderThickness = new Thickness(2),
|
||||||
|
Background = Brushes.White,
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F))
|
||||||
|
};
|
||||||
|
toggleBtn.Click += (_, _) => buttonDef.Execute();
|
||||||
|
buttonDef.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||||
|
toggleBtn.IsVisible = buttonDef.IsVisible;
|
||||||
|
else if (args.PropertyName == nameof(ButtonDefinition.IsToggled))
|
||||||
|
toggleBtn.IsChecked = buttonDef.IsToggled;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
control = toggleBtn;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Content = buttonDef.Title.ToUpperInvariant(),
|
||||||
|
Width = 176,
|
||||||
|
Height = 48,
|
||||||
|
FontSize = 14,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
|
||||||
|
IsVisible = buttonDef.IsVisible,
|
||||||
|
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)),
|
||||||
|
BorderThickness = new Thickness(2),
|
||||||
|
Background = Brushes.White,
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F))
|
||||||
|
};
|
||||||
|
btn.Click += (_, _) => buttonDef.Execute();
|
||||||
|
buttonDef.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||||
|
btn.IsVisible = buttonDef.IsVisible;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
control = btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert before BtnMinimize
|
||||||
|
var minimizeIndex = BottomBarPanel.Children.IndexOf(BtnMinimize);
|
||||||
|
if (minimizeIndex >= 0)
|
||||||
|
BottomBarPanel.Children.Insert(minimizeIndex, control);
|
||||||
|
else
|
||||||
|
BottomBarPanel.Children.Add(control);
|
||||||
|
|
||||||
|
_dynamicButtonControls[buttonDef.Key] = control;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
if (_dynamicButtonControls.TryGetValue(buttonDef.Key, out var ctrl))
|
||||||
|
{
|
||||||
|
BottomBarPanel.Children.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Remove(buttonDef.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
VisionBuilder.UI.Avalonia.Uno/Views/PasswordDialog.axaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.PasswordDialog"
|
||||||
|
Title="Password Required"
|
||||||
|
Width="400" Height="180"
|
||||||
|
CanResize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,Auto,Auto" Margin="16">
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="Enter password:"
|
||||||
|
FontSize="14"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
|
||||||
|
<TextBox Grid.Row="1"
|
||||||
|
x:Name="TxtPassword"
|
||||||
|
PasswordChar="*"
|
||||||
|
Margin="0,0,0,16" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Spacing="8">
|
||||||
|
<Button x:Name="BtnOk"
|
||||||
|
Content="OK"
|
||||||
|
Width="80" Height="36"
|
||||||
|
HorizontalContentAlignment="Center" />
|
||||||
|
<Button x:Name="BtnCancel"
|
||||||
|
Content="Cancel"
|
||||||
|
Width="80" Height="36"
|
||||||
|
HorizontalContentAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
23
VisionBuilder.UI.Avalonia.Uno/Views/PasswordDialog.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
|
||||||
|
public partial class PasswordDialog : Window
|
||||||
|
{
|
||||||
|
public PasswordDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
BtnOk.Click += (_, _) => Close(TxtPassword.Text);
|
||||||
|
BtnCancel.Click += (_, _) => Close(null);
|
||||||
|
|
||||||
|
TxtPassword.KeyDown += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Enter)
|
||||||
|
Close(TxtPassword.Text);
|
||||||
|
else if (e.Key == Key.Escape)
|
||||||
|
Close(null);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.RecipeSelectionDialog"
|
||||||
|
Title="Select Recipe"
|
||||||
|
Width="800" Height="600"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="White">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto">
|
||||||
|
<ListBox x:Name="RecipeList"
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="8"
|
||||||
|
Background="White"
|
||||||
|
SelectionMode="Single">
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="8" Spacing="8">
|
||||||
|
<Button x:Name="BtnCreateNew"
|
||||||
|
Content="CREATE NEW RECIPE"
|
||||||
|
IsVisible="False"
|
||||||
|
Height="48" Padding="16,0"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
<Button x:Name="BtnCancel"
|
||||||
|
Content="CANCEL"
|
||||||
|
Height="48" Width="176" Padding="16,0"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
|
||||||
|
public partial class RecipeSelectionDialog : global::Avalonia.Controls.Window
|
||||||
|
{
|
||||||
|
private readonly RecipeSelectionVM _recipeSelectionVm;
|
||||||
|
private readonly IRecipeCreationTool _recipeCreationTool;
|
||||||
|
private readonly TaskCompletionSource<bool> _tcs;
|
||||||
|
|
||||||
|
public RecipeSelectionDialog(RecipeSelectionVM recipeSelectionVm, IRecipeCreationTool recipeCreationTool, TaskCompletionSource<bool> tcs)
|
||||||
|
{
|
||||||
|
_recipeSelectionVm = recipeSelectionVm;
|
||||||
|
_recipeCreationTool = recipeCreationTool;
|
||||||
|
_tcs = tcs;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
if (_recipeCreationTool.Enabled)
|
||||||
|
{
|
||||||
|
BtnCreateNew.IsVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BtnCancel.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
_tcs.TrySetResult(false);
|
||||||
|
Close();
|
||||||
|
};
|
||||||
|
BtnCreateNew.Click += BtnCreateNew_Click;
|
||||||
|
|
||||||
|
Closed += (_, _) => _tcs.TrySetResult(false);
|
||||||
|
|
||||||
|
LoadRecipes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadRecipes()
|
||||||
|
{
|
||||||
|
var recipes = _recipeSelectionVm.Recipes.OrderBy(x => x.RecipeName).ToList();
|
||||||
|
var items = new List<RecipeItem>();
|
||||||
|
|
||||||
|
foreach (var recipe in recipes)
|
||||||
|
{
|
||||||
|
Bitmap? thumbnail = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (recipe.Image != null && !recipe.Image.Empty())
|
||||||
|
{
|
||||||
|
thumbnail = ImageConverter.MatToAvaloniaBitmap(
|
||||||
|
recipe.Image.Resize(new OpenCvSharp.Size(128, 128)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore conversion errors
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add(new RecipeItem(recipe, thumbnail));
|
||||||
|
}
|
||||||
|
|
||||||
|
RecipeList.ItemsSource = items;
|
||||||
|
|
||||||
|
RecipeList.ItemTemplate = new global::Avalonia.Controls.Templates.FuncDataTemplate<RecipeItem>((item, _) =>
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Width = 140,
|
||||||
|
Margin = new global::Avalonia.Thickness(4),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
var image = new Image
|
||||||
|
{
|
||||||
|
Source = item.Thumbnail,
|
||||||
|
Width = 128,
|
||||||
|
Height = 128,
|
||||||
|
Stretch = Stretch.Uniform
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item.Thumbnail == null)
|
||||||
|
{
|
||||||
|
image.Source = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = new TextBlock
|
||||||
|
{
|
||||||
|
Text = item.Recipe.RecipeName,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
TextTrimming = global::Avalonia.Media.TextTrimming.CharacterEllipsis,
|
||||||
|
FontSize = 12,
|
||||||
|
Margin = new global::Avalonia.Thickness(0, 4, 0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.Children.Add(image);
|
||||||
|
panel.Children.Add(text);
|
||||||
|
return panel;
|
||||||
|
});
|
||||||
|
|
||||||
|
RecipeList.SelectionChanged += RecipeList_SelectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecipeList_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (RecipeList.SelectedItem is RecipeItem item)
|
||||||
|
{
|
||||||
|
_recipeSelectionVm.SelectedRecipe = item.Recipe;
|
||||||
|
_tcs.TrySetResult(true);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnCreateNew_Click(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_recipeCreationTool.CreateRecipe(out var recipeName))
|
||||||
|
{
|
||||||
|
_recipeSelectionVm.SelectedRecipe = new RecipeData { RecipeName = recipeName };
|
||||||
|
_tcs.TrySetResult(true);
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RecipeItem(RecipeData Recipe, Bitmap? Thumbnail);
|
||||||
|
}
|
||||||
126
VisionBuilder.UI.Avalonia.Uno/Views/SingleCameraView.axaml
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="using:VisionBuilder.UI.Avalonia.Uno.Converters"
|
||||||
|
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.SingleCameraView"
|
||||||
|
Background="White">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:MatToBitmapConverter x:Key="MatToBitmapConverter" />
|
||||||
|
<converters:BoolToVisibilityConverter x:Key="BoolToVis" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="5*,1,2*,1,Auto,Auto">
|
||||||
|
<!-- Column 0: Preview -->
|
||||||
|
<Grid Grid.Column="0" RowDefinitions="Auto,*">
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
x:Name="LblCameraName"
|
||||||
|
Text="Camera"
|
||||||
|
FontSize="16" FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,8,0,4" />
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Margin="4"
|
||||||
|
x:Name="PreviewBorder"
|
||||||
|
BorderThickness="0"
|
||||||
|
BorderBrush="Red">
|
||||||
|
<Image x:Name="PreviewImage"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Column 1: Divider -->
|
||||||
|
<Border Grid.Column="1" Background="#D32F2F" />
|
||||||
|
|
||||||
|
<!-- Column 2: Errors -->
|
||||||
|
<Grid Grid.Column="2" RowDefinitions="Auto,*">
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="Errors"
|
||||||
|
FontSize="16"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,8,0,4" />
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsControl x:Name="ErrorsList">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Column 3: Divider -->
|
||||||
|
<Border Grid.Column="3" Background="#D32F2F" />
|
||||||
|
|
||||||
|
<!-- Column 4: Stats -->
|
||||||
|
<ScrollViewer Grid.Column="4" Width="220" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel x:Name="StatsPanel" Margin="8,8,8,0" Spacing="2" />
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Column 5: Action buttons (right side) -->
|
||||||
|
<StackPanel x:Name="ButtonsPanel" Grid.Column="5" Width="180" Margin="8" Spacing="6"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<Button x:Name="BtnSelectRecipe"
|
||||||
|
Content="SELECT RECIPE"
|
||||||
|
Height="60"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
<Button x:Name="BtnStart"
|
||||||
|
Content="START"
|
||||||
|
Height="60"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
Background="#D32F2F" Foreground="White" />
|
||||||
|
<Button x:Name="BtnStop"
|
||||||
|
Content="STOP"
|
||||||
|
Height="60"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
Background="#D32F2F" Foreground="White" />
|
||||||
|
<Button x:Name="BtnPause"
|
||||||
|
Content="PAUSE"
|
||||||
|
Height="60"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
<Button x:Name="BtnResume"
|
||||||
|
Content="RESUME"
|
||||||
|
Height="60"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
<Button x:Name="BtnHotReload"
|
||||||
|
Content="HOT RELOAD"
|
||||||
|
Height="60"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
FontSize="14" FontWeight="Bold"
|
||||||
|
BorderBrush="#D32F2F"
|
||||||
|
BorderThickness="2"
|
||||||
|
Background="White"
|
||||||
|
Foreground="#D32F2F" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
415
VisionBuilder.UI.Avalonia.Uno/Views/SingleCameraView.axaml.cs
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using OpenCvSharp;
|
||||||
|
using VisionBuilder.UI.Avalonia.Uno.Services;
|
||||||
|
using VisionBuilder.UI.Common;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||||
|
|
||||||
|
public partial class SingleCameraView : UserControl
|
||||||
|
{
|
||||||
|
private SingleCameraVM? _vm;
|
||||||
|
private ObservableCollection<ErrorData>? _subscribedErrors;
|
||||||
|
private readonly Dictionary<string, (TextBlock label, TextBlock value)> _statsFields = new();
|
||||||
|
private readonly Dictionary<string, Control> _dynamicButtonControls = new();
|
||||||
|
|
||||||
|
private const string StartedAtLabel = "Started at";
|
||||||
|
private const string ProcessingTimeLabel = "Processing time";
|
||||||
|
private const string BadLabel = "Bad";
|
||||||
|
private const string TotalLabel = "Total";
|
||||||
|
private const string ErrorRateLabel = "Error rate";
|
||||||
|
private const string DetailsLabel = "Details";
|
||||||
|
|
||||||
|
public SingleCameraView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetViewModel(SingleCameraVM singleCameraVm)
|
||||||
|
{
|
||||||
|
_vm = singleCameraVm;
|
||||||
|
_vm.SynchronizationContext = SynchronizationContext.Current;
|
||||||
|
|
||||||
|
DataContext = _vm;
|
||||||
|
|
||||||
|
// Bind commands
|
||||||
|
BtnSelectRecipe.Click += async (_, _) => await _vm.SelectRecipe();
|
||||||
|
BtnStart.Click += (_, _) => _vm.StartCommand.Execute(null);
|
||||||
|
BtnStop.Click += (_, _) => _vm.StopCommand.Execute(null);
|
||||||
|
BtnPause.Click += (_, _) => _vm.PauseCommand.Execute(null);
|
||||||
|
BtnResume.Click += (_, _) => _vm.ResumeCommand.Execute(null);
|
||||||
|
BtnHotReload.Click += (_, _) => _vm.HotReloadCommand.Execute(null);
|
||||||
|
|
||||||
|
// Subscribe to VM property changes
|
||||||
|
_vm.PropertyChanged += Vm_PropertyChanged;
|
||||||
|
_vm.PreviewVm.PropertyChanged += PreviewVm_PropertyChanged;
|
||||||
|
_vm.StatisticsVm.PropertyChanged += StatisticsVm_PropertyChanged;
|
||||||
|
|
||||||
|
// Subscribe to errors collection
|
||||||
|
_subscribedErrors = _vm.ErrorsVm.Errors;
|
||||||
|
_subscribedErrors.CollectionChanged += Errors_CollectionChanged;
|
||||||
|
|
||||||
|
// Initial UI state
|
||||||
|
UpdateButtonVisibility();
|
||||||
|
UpdateCameraLabel();
|
||||||
|
InitializeStatsFields();
|
||||||
|
|
||||||
|
// Subscribe to dynamic buttons
|
||||||
|
_vm.DynamicButtons.CollectionChanged += DynamicButtons_CollectionChanged;
|
||||||
|
foreach (var btn in _vm.DynamicButtons)
|
||||||
|
AddDynamicButton(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Vm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
switch (e.PropertyName)
|
||||||
|
{
|
||||||
|
case nameof(SingleCameraVM.CameraLabelAndStatus):
|
||||||
|
UpdateCameraLabel();
|
||||||
|
break;
|
||||||
|
case nameof(SingleCameraVM.CurrentRecipeName):
|
||||||
|
BtnSelectRecipe.Content = _vm!.CurrentRecipeName.ToUpperInvariant();
|
||||||
|
break;
|
||||||
|
case nameof(SingleCameraVM.CanStart):
|
||||||
|
case nameof(SingleCameraVM.CanStop):
|
||||||
|
case nameof(SingleCameraVM.CanPause):
|
||||||
|
case nameof(SingleCameraVM.CanResume):
|
||||||
|
case nameof(SingleCameraVM.CanHotReload):
|
||||||
|
case nameof(SingleCameraVM.RecipeNotSelected):
|
||||||
|
UpdateButtonVisibility();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCameraLabel()
|
||||||
|
{
|
||||||
|
LblCameraName.Text = _vm?.CameraLabelAndStatus ?? "Camera";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateButtonVisibility()
|
||||||
|
{
|
||||||
|
if (_vm == null) return;
|
||||||
|
BtnStart.IsVisible = _vm.CanStart;
|
||||||
|
BtnStop.IsVisible = _vm.CanStop;
|
||||||
|
BtnPause.IsVisible = _vm.CanPause;
|
||||||
|
BtnResume.IsVisible = _vm.CanResume;
|
||||||
|
BtnHotReload.IsVisible = _vm.CanHotReload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreviewVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(PreviewVM.ImagePreview))
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
var mat = _vm?.PreviewVm.ImagePreview;
|
||||||
|
if (mat != null && !mat.Empty())
|
||||||
|
{
|
||||||
|
PreviewImage.Source = ImageConverter.MatToAvaloniaBitmap(mat);
|
||||||
|
}
|
||||||
|
|
||||||
|
var isError = _vm?.PreviewVm.IsError ?? false;
|
||||||
|
PreviewBorder.BorderThickness = isError ? new Thickness(4) : new Thickness(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Statistics
|
||||||
|
|
||||||
|
private void InitializeStatsFields()
|
||||||
|
{
|
||||||
|
StatsPanel.Children.Clear();
|
||||||
|
_statsFields.Clear();
|
||||||
|
UpdateStat(StartedAtLabel, "");
|
||||||
|
UpdateStat(ProcessingTimeLabel, "");
|
||||||
|
UpdateStat(TotalLabel, "0");
|
||||||
|
UpdateStat(BadLabel, "0");
|
||||||
|
UpdateStat(ErrorRateLabel, "0");
|
||||||
|
UpdateStat(DetailsLabel, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StatisticsVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (_vm == null) return;
|
||||||
|
var stats = _vm.StatisticsVm;
|
||||||
|
switch (e.PropertyName)
|
||||||
|
{
|
||||||
|
case nameof(StatisticsVM.SessionStarted):
|
||||||
|
InitializeStatsFields();
|
||||||
|
UpdateStat(StartedAtLabel, stats.SessionStarted);
|
||||||
|
break;
|
||||||
|
case nameof(StatisticsVM.ProcessingTime):
|
||||||
|
UpdateStat(ProcessingTimeLabel, stats.ProcessingTime);
|
||||||
|
break;
|
||||||
|
case nameof(StatisticsVM.Bad):
|
||||||
|
UpdateStat(BadLabel, stats.Bad.ToString());
|
||||||
|
break;
|
||||||
|
case nameof(StatisticsVM.Total):
|
||||||
|
UpdateStat(TotalLabel, stats.Total.ToString());
|
||||||
|
break;
|
||||||
|
case nameof(StatisticsVM.ErrorRate):
|
||||||
|
UpdateStat(ErrorRateLabel, stats.ErrorRate);
|
||||||
|
break;
|
||||||
|
case nameof(StatisticsVM.StatisticsDetails):
|
||||||
|
UpdateStat(DetailsLabel, stats.StatisticsDetails);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStat(string name, string text)
|
||||||
|
{
|
||||||
|
if (!_statsFields.ContainsKey(name))
|
||||||
|
{
|
||||||
|
var label = new TextBlock
|
||||||
|
{
|
||||||
|
Text = name + ":",
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
FontSize = 12,
|
||||||
|
Margin = new Thickness(0, 6, 0, 0)
|
||||||
|
};
|
||||||
|
var value = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontSize = 12,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Margin = new Thickness(0)
|
||||||
|
};
|
||||||
|
StatsPanel.Children.Add(label);
|
||||||
|
StatsPanel.Children.Add(value);
|
||||||
|
_statsFields[name] = (label, value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_statsFields[name].value.Text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Errors
|
||||||
|
|
||||||
|
private void Errors_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
if (e.NewItems != null)
|
||||||
|
{
|
||||||
|
foreach (ErrorData error in e.NewItems)
|
||||||
|
AddErrorThumbnail(error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
if (e.OldItems != null)
|
||||||
|
{
|
||||||
|
foreach (ErrorData error in e.OldItems)
|
||||||
|
RemoveErrorThumbnail(error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Reset:
|
||||||
|
ErrorsList.ItemsSource = null;
|
||||||
|
_errorControls.Clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<ErrorData, Control> _errorControls = new();
|
||||||
|
|
||||||
|
private void AddErrorThumbnail(ErrorData errorData)
|
||||||
|
{
|
||||||
|
var panel = new StackPanel
|
||||||
|
{
|
||||||
|
Width = 183,
|
||||||
|
Margin = new Thickness(4),
|
||||||
|
Cursor = new Cursor(StandardCursorType.Hand)
|
||||||
|
};
|
||||||
|
|
||||||
|
Bitmap? thumbnail = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (errorData.ImageAnalysis != null && !errorData.ImageAnalysis.Empty())
|
||||||
|
{
|
||||||
|
thumbnail = ImageConverter.MatToAvaloniaBitmap(
|
||||||
|
errorData.ImageAnalysis.Resize(new OpenCvSharp.Size(183, 138)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore conversion errors
|
||||||
|
}
|
||||||
|
|
||||||
|
var image = new Image
|
||||||
|
{
|
||||||
|
Source = thumbnail,
|
||||||
|
Height = 138,
|
||||||
|
Stretch = Stretch.Uniform
|
||||||
|
};
|
||||||
|
|
||||||
|
var text = new TextBlock
|
||||||
|
{
|
||||||
|
Text = errorData.Title,
|
||||||
|
FontSize = 11,
|
||||||
|
HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
TextTrimming = TextTrimming.CharacterEllipsis
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.Children.Add(image);
|
||||||
|
panel.Children.Add(text);
|
||||||
|
|
||||||
|
panel.PointerPressed += (_, _) =>
|
||||||
|
{
|
||||||
|
_vm?.ErrorsVm.ShowImage(errorData);
|
||||||
|
};
|
||||||
|
|
||||||
|
_errorControls[errorData] = panel;
|
||||||
|
|
||||||
|
// Insert at the beginning (newest first)
|
||||||
|
if (ErrorsList.ItemsSource == null)
|
||||||
|
{
|
||||||
|
var items = new ObservableCollection<Control>();
|
||||||
|
ErrorsList.ItemsSource = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ErrorsList.ItemsSource is ObservableCollection<Control> collection)
|
||||||
|
{
|
||||||
|
collection.Insert(0, panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveErrorThumbnail(ErrorData errorData)
|
||||||
|
{
|
||||||
|
if (_errorControls.TryGetValue(errorData, out var control))
|
||||||
|
{
|
||||||
|
if (ErrorsList.ItemsSource is ObservableCollection<Control> collection)
|
||||||
|
{
|
||||||
|
collection.Remove(control);
|
||||||
|
}
|
||||||
|
_errorControls.Remove(errorData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Dynamic Buttons
|
||||||
|
|
||||||
|
private void DynamicButtons_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (ButtonDefinition btn in e.NewItems!)
|
||||||
|
AddDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
foreach (ButtonDefinition btn in e.OldItems!)
|
||||||
|
RemoveDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Reset:
|
||||||
|
foreach (var ctrl in _dynamicButtonControls.Values)
|
||||||
|
ButtonsPanel.Children.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
Control control;
|
||||||
|
if (buttonDef.Type == EButtonType.Toggle)
|
||||||
|
{
|
||||||
|
var toggleBtn = new ToggleButton
|
||||||
|
{
|
||||||
|
Content = buttonDef.Title.ToUpperInvariant(),
|
||||||
|
Height = 60,
|
||||||
|
HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Stretch,
|
||||||
|
HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
|
||||||
|
FontSize = 14,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
IsChecked = buttonDef.IsToggled,
|
||||||
|
IsVisible = buttonDef.IsVisible,
|
||||||
|
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)),
|
||||||
|
BorderThickness = new Thickness(2),
|
||||||
|
Background = Brushes.White,
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F))
|
||||||
|
};
|
||||||
|
toggleBtn.Click += (_, _) => buttonDef.Execute();
|
||||||
|
buttonDef.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||||
|
toggleBtn.IsVisible = buttonDef.IsVisible;
|
||||||
|
else if (args.PropertyName == nameof(ButtonDefinition.IsToggled))
|
||||||
|
toggleBtn.IsChecked = buttonDef.IsToggled;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
control = toggleBtn;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Content = buttonDef.Title.ToUpperInvariant(),
|
||||||
|
Height = 60,
|
||||||
|
HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Stretch,
|
||||||
|
HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
|
||||||
|
FontSize = 14,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
IsVisible = buttonDef.IsVisible,
|
||||||
|
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)),
|
||||||
|
BorderThickness = new Thickness(2),
|
||||||
|
Background = Brushes.White,
|
||||||
|
Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F))
|
||||||
|
};
|
||||||
|
btn.Click += (_, _) => buttonDef.Execute();
|
||||||
|
buttonDef.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||||
|
btn.IsVisible = buttonDef.IsVisible;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
control = btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonsPanel.Children.Add(control);
|
||||||
|
_dynamicButtonControls[buttonDef.Key] = control;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
if (_dynamicButtonControls.TryGetValue(buttonDef.Key, out var ctrl))
|
||||||
|
{
|
||||||
|
ButtonsPanel.Children.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Remove(buttonDef.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<!--<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>-->
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Deterministic>false</Deterministic>
|
||||||
|
<Version>2.0.14</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.LinuxFramebuffer" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Ninject.Extensions.ChildKernel" Version="3.3.0" />
|
||||||
|
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
|
||||||
|
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20241108" />
|
||||||
|
<PackageReference Include="swety.OpenCvSharp4.runtime.linux-arm64" Version="4.10.0.20241108" />
|
||||||
|
<!-- Override transitive GPU OnnxRuntime with CPU-only (GPU has no linux-arm64 natives) -->
|
||||||
|
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu" Version="1.18.0" ExcludeAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu.Windows" Version="1.18.0" ExcludeAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu.Linux" Version="1.18.0" ExcludeAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.18.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\framework\Inspectron.Settings\Inspectron.Settings.csproj" />
|
||||||
|
<ProjectReference Include="..\Hawkeye.VisionBuilder.UI.Sources.Emulation\Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj" />
|
||||||
|
<ProjectReference Include="..\VisionBuilder.UI.Camera\VisionBuilder.UI.Camera.csproj" />
|
||||||
|
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\VisionBuilder.UI.IOCommander\VisionBuilder.UI.IOCommander.csproj" />
|
||||||
|
<ProjectReference Include="..\VisionBuilder.UI.Recipes.HawkeyeRecipe\VisionBuilder.UI.Recipes.HawkeyeRecipe.csproj" />
|
||||||
|
<ProjectReference Include="..\VisionBuilder.UI.Ringbuffer\VisionBuilder.UI.Ringbuffer.csproj" />
|
||||||
|
<ProjectReference Include="..\VisionBuilder.UI.Statistics\VisionBuilder.UI.Statistics.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -33,46 +33,41 @@
|
|||||||
OnClick="@OnSelectRecipe">
|
OnClick="@OnSelectRecipe">
|
||||||
@_currentRecipeName
|
@_currentRecipeName
|
||||||
</MudButton>
|
</MudButton>
|
||||||
@if (_canStart)
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||||
{
|
Class="camera-btn"
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
Style="@(_canStart ? null : "display:none")"
|
||||||
Class="camera-btn"
|
Disabled="@(!_canStart)"
|
||||||
OnClick="@OnStart">
|
OnClick="@OnStart">
|
||||||
Start
|
Start
|
||||||
</MudButton>
|
</MudButton>
|
||||||
}
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||||
@if (_canStop)
|
Class="camera-btn"
|
||||||
{
|
Style="@(_canStop ? null : "display:none")"
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
Disabled="@(!_canStop)"
|
||||||
Class="camera-btn"
|
OnClick="@OnStop">
|
||||||
OnClick="@OnStop">
|
Stop
|
||||||
Stop
|
</MudButton>
|
||||||
</MudButton>
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||||
}
|
Class="camera-btn"
|
||||||
@if (_showPauseButton && _canPause)
|
Style="@(_showPauseButton && _canPause ? null : "display:none")"
|
||||||
{
|
Disabled="@(!_canPause)"
|
||||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
OnClick="@OnPause">
|
||||||
Class="camera-btn"
|
Pause
|
||||||
OnClick="@OnPause">
|
</MudButton>
|
||||||
Pause
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||||
</MudButton>
|
Class="camera-btn"
|
||||||
}
|
Style="@(_showPauseButton && _canResume ? null : "display:none")"
|
||||||
@if (_showPauseButton && _canResume)
|
Disabled="@(!_canResume)"
|
||||||
{
|
OnClick="@OnResume">
|
||||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
Resume
|
||||||
Class="camera-btn"
|
</MudButton>
|
||||||
OnClick="@OnResume">
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||||
Resume
|
Class="camera-btn"
|
||||||
</MudButton>
|
Style="@(_canHotReload ? null : "display:none")"
|
||||||
}
|
Disabled="@(!_canHotReload)"
|
||||||
@if (_canHotReload)
|
OnClick="@OnHotReload">
|
||||||
{
|
Hot Reload
|
||||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
</MudButton>
|
||||||
Class="camera-btn"
|
|
||||||
OnClick="@OnHotReload">
|
|
||||||
Hot Reload
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
|
||||||
|
public enum EButtonType
|
||||||
|
{
|
||||||
|
Click,
|
||||||
|
Toggle
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class ButtonDefinition : ObservableObject
|
||||||
|
{
|
||||||
|
public string Key { get; }
|
||||||
|
public string Title { get; }
|
||||||
|
public EButtonType Type { get; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isVisible = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isToggled;
|
||||||
|
|
||||||
|
private readonly Action? _clickCallback;
|
||||||
|
private readonly Action<bool>? _toggleCallback;
|
||||||
|
|
||||||
|
public ButtonDefinition(string key, string title, Action clickCallback)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Title = title;
|
||||||
|
Type = EButtonType.Click;
|
||||||
|
_clickCallback = clickCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ButtonDefinition(string key, string title, Action<bool> toggleCallback, bool initialState = false)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Title = title;
|
||||||
|
Type = EButtonType.Toggle;
|
||||||
|
_toggleCallback = toggleCallback;
|
||||||
|
_isToggled = initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
if (Type == EButtonType.Click)
|
||||||
|
{
|
||||||
|
_clickCallback?.Invoke();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IsToggled = !IsToggled;
|
||||||
|
_toggleCallback?.Invoke(IsToggled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using VisionBuilder.UI.Common.Commands;
|
using VisionBuilder.UI.Common.Commands;
|
||||||
using VisionBuilder.UI.Common.Commands.Interfaces;
|
using VisionBuilder.UI.Common.Commands.Interfaces;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||||
|
|
||||||
namespace VisionBuilder.UI.Common.ViewModel;
|
namespace VisionBuilder.UI.Common.ViewModel;
|
||||||
@@ -25,6 +26,27 @@ public partial class MainWindowVM:ObservableObject
|
|||||||
public ObservableCollection<SingleCameraVM> SingleCameraVms { get; set; } =
|
public ObservableCollection<SingleCameraVM> SingleCameraVms { get; set; } =
|
||||||
new ObservableCollection<SingleCameraVM>();
|
new ObservableCollection<SingleCameraVM>();
|
||||||
|
|
||||||
|
public ObservableCollection<ButtonDefinition> DynamicButtons { get; } = new();
|
||||||
|
|
||||||
|
public void AddButton(ButtonDefinition button)
|
||||||
|
{
|
||||||
|
DynamicButtons.Add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetButtonVisibility(string key, bool isVisible)
|
||||||
|
{
|
||||||
|
var button = DynamicButtons.FirstOrDefault(b => b.Key == key);
|
||||||
|
if (button != null)
|
||||||
|
button.IsVisible = isVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveButton(string key)
|
||||||
|
{
|
||||||
|
var button = DynamicButtons.FirstOrDefault(b => b.Key == key);
|
||||||
|
if (button != null)
|
||||||
|
DynamicButtons.Remove(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
public async Task Settings()
|
public async Task Settings()
|
||||||
|
|||||||
@@ -24,6 +24,27 @@ namespace VisionBuilder.UI.Common
|
|||||||
public StatisticsVM StatisticsVm { get; private set; }
|
public StatisticsVM StatisticsVm { get; private set; }
|
||||||
public PreviewVM PreviewVm { get; private set; }
|
public PreviewVM PreviewVm { get; private set; }
|
||||||
|
|
||||||
|
public ObservableCollection<ButtonDefinition> DynamicButtons { get; } = new();
|
||||||
|
|
||||||
|
public void AddButton(ButtonDefinition button)
|
||||||
|
{
|
||||||
|
DynamicButtons.Add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetButtonVisibility(string key, bool isVisible)
|
||||||
|
{
|
||||||
|
var button = DynamicButtons.FirstOrDefault(b => b.Key == key);
|
||||||
|
if (button != null)
|
||||||
|
button.IsVisible = isVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveButton(string key)
|
||||||
|
{
|
||||||
|
var button = DynamicButtons.FirstOrDefault(b => b.Key == key);
|
||||||
|
if (button != null)
|
||||||
|
DynamicButtons.Remove(button);
|
||||||
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _currentRecipeName = "Select recipe";
|
private string _currentRecipeName = "Select recipe";
|
||||||
|
|
||||||
@@ -147,8 +168,6 @@ namespace VisionBuilder.UI.Common
|
|||||||
public async Task Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IsRunning = true;
|
IsRunning = true;
|
||||||
await Task.Run(() => { _recognitionControl.Start(); });
|
await Task.Run(() => { _recognitionControl.Start(); });
|
||||||
_handlingEnabled = true;
|
_handlingEnabled = true;
|
||||||
|
|||||||
7
VisionBuilder.UI.RaspberryAcquisition/App.axaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="VisionBuilder.UI.RaspberryAcquisition.App">
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
28
VisionBuilder.UI.RaspberryAcquisition/App.axaml.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using VisionBuilder.UI.RaspberryAcquisition.ViewModels;
|
||||||
|
using VisionBuilder.UI.RaspberryAcquisition.Views;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition;
|
||||||
|
|
||||||
|
public class App : Application
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow
|
||||||
|
{
|
||||||
|
DataContext = new MainWindowViewModel()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
@@ -0,0 +1,10 @@
|
|||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition.Models;
|
||||||
|
|
||||||
|
public class CalibrationData
|
||||||
|
{
|
||||||
|
public double[] CameraMatrix { get; set; } = [];
|
||||||
|
public double[] DistCoeffs { get; set; } = [];
|
||||||
|
public double RmsError { get; set; }
|
||||||
|
public int ImageWidth { get; set; }
|
||||||
|
public int ImageHeight { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition.Models;
|
||||||
|
|
||||||
|
public class CaptureSettings
|
||||||
|
{
|
||||||
|
public int Width { get; set; } = 2028;
|
||||||
|
public int Height { get; set; } = 1520;
|
||||||
|
public int ShutterSpeed { get; set; } = 10000;
|
||||||
|
public int Framerate { get; set; } = 2;
|
||||||
|
public double AwbGainRed { get; set; } = 3.86;
|
||||||
|
public double AwbGainBlue { get; set; } = 1.46;
|
||||||
|
|
||||||
|
public string BuildArguments()
|
||||||
|
{
|
||||||
|
return $"--codec mjpeg -t0 --width {Width} --height {Height} " +
|
||||||
|
$"--shutter {ShutterSpeed} --framerate {Framerate} " +
|
||||||
|
$"--awbgains {AwbGainRed:F2},{AwbGainBlue:F2} --nopreview -o -";
|
||||||
|
}
|
||||||
|
}
|
||||||
21
VisionBuilder.UI.RaspberryAcquisition/Program.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.LinuxFramebuffer;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Contains("--drm"))
|
||||||
|
BuildAvaloniaApp().StartLinuxDrm(args, card: null, scaling: 1.0);
|
||||||
|
else
|
||||||
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
=> AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.LogToTrace();
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using OpenCvSharp;
|
||||||
|
using VisionBuilder.UI.RaspberryAcquisition.Models;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition.Services;
|
||||||
|
|
||||||
|
public static class CameraCalibrationService
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
public static CalibrationData Calibrate(string imageDirectory, Size patternSize)
|
||||||
|
{
|
||||||
|
var imageFiles = Directory.GetFiles(imageDirectory, "*.png");
|
||||||
|
if (imageFiles.Length == 0)
|
||||||
|
throw new InvalidOperationException($"No PNG images found in {imageDirectory}");
|
||||||
|
|
||||||
|
var objectPointsList = new List<Mat>();
|
||||||
|
var imagePointsList = new List<Mat>();
|
||||||
|
Size imageSize = default;
|
||||||
|
|
||||||
|
// Build the 3D object points for the checkerboard (z=0 plane)
|
||||||
|
int cornerCount = patternSize.Width * patternSize.Height;
|
||||||
|
var objPts = new Point3f[cornerCount];
|
||||||
|
for (int row = 0; row < patternSize.Height; row++)
|
||||||
|
for (int col = 0; col < patternSize.Width; col++)
|
||||||
|
objPts[row * patternSize.Width + col] = new Point3f(col, row, 0);
|
||||||
|
|
||||||
|
int found = 0;
|
||||||
|
foreach (var file in imageFiles)
|
||||||
|
{
|
||||||
|
using var image = Cv2.ImRead(file);
|
||||||
|
using var gray = new Mat();
|
||||||
|
Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
|
||||||
|
imageSize = new Size(image.Width, image.Height);
|
||||||
|
|
||||||
|
if (Cv2.FindChessboardCorners(gray, patternSize, out var corners,
|
||||||
|
ChessboardFlags.AdaptiveThresh | ChessboardFlags.FastCheck))
|
||||||
|
{
|
||||||
|
Cv2.CornerSubPix(gray, corners,
|
||||||
|
new Size(11, 11), new Size(-1, -1),
|
||||||
|
new TermCriteria(CriteriaTypes.Eps | CriteriaTypes.MaxIter, 30, 0.001));
|
||||||
|
|
||||||
|
// Convert to Mat for CalibrateCamera
|
||||||
|
var objMat = new Mat(cornerCount, 1, MatType.CV_32FC3);
|
||||||
|
for (int i = 0; i < cornerCount; i++)
|
||||||
|
objMat.Set(i, 0, objPts[i]);
|
||||||
|
objectPointsList.Add(objMat);
|
||||||
|
|
||||||
|
var imgMat = new Mat(corners.Length, 1, MatType.CV_32FC2);
|
||||||
|
for (int i = 0; i < corners.Length; i++)
|
||||||
|
imgMat.Set(i, 0, corners[i]);
|
||||||
|
imagePointsList.Add(imgMat);
|
||||||
|
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found == 0)
|
||||||
|
throw new InvalidOperationException("Checkerboard corners not found in any image");
|
||||||
|
|
||||||
|
var cameraMatrix = new Mat();
|
||||||
|
var distCoeffs = new Mat();
|
||||||
|
double rms = Cv2.CalibrateCamera(
|
||||||
|
objectPointsList, imagePointsList, imageSize,
|
||||||
|
cameraMatrix, distCoeffs,
|
||||||
|
out _, out _);
|
||||||
|
|
||||||
|
var data = new CalibrationData
|
||||||
|
{
|
||||||
|
CameraMatrix = MatToArray(cameraMatrix),
|
||||||
|
DistCoeffs = MatToArray(distCoeffs),
|
||||||
|
RmsError = rms,
|
||||||
|
ImageWidth = imageSize.Width,
|
||||||
|
ImageHeight = imageSize.Height
|
||||||
|
};
|
||||||
|
|
||||||
|
cameraMatrix.Dispose();
|
||||||
|
distCoeffs.Dispose();
|
||||||
|
foreach (var m in objectPointsList) m.Dispose();
|
||||||
|
foreach (var m in imagePointsList) m.Dispose();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SaveCalibration(CalibrationData data, string path)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(data, JsonOptions);
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (Mat cameraMatrix, Mat distCoeffs) LoadCalibration(string path)
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
var data = JsonSerializer.Deserialize<CalibrationData>(json)
|
||||||
|
?? throw new InvalidOperationException("Failed to deserialize calibration data");
|
||||||
|
|
||||||
|
var cameraMatrix = new Mat(3, 3, MatType.CV_64F);
|
||||||
|
for (int i = 0; i < 9; i++)
|
||||||
|
cameraMatrix.Set(i / 3, i % 3, data.CameraMatrix[i]);
|
||||||
|
|
||||||
|
var distCoeffs = new Mat(1, data.DistCoeffs.Length, MatType.CV_64F);
|
||||||
|
for (int i = 0; i < data.DistCoeffs.Length; i++)
|
||||||
|
distCoeffs.Set(0, i, data.DistCoeffs[i]);
|
||||||
|
|
||||||
|
return (cameraMatrix, distCoeffs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double[] MatToArray(Mat mat)
|
||||||
|
{
|
||||||
|
var total = mat.Rows * mat.Cols;
|
||||||
|
var arr = new double[total];
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
arr[i] = mat.At<double>(i / mat.Cols, i % mat.Cols);
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using OpenCvSharp;
|
||||||
|
using VisionBuilder.UI.Common.Processing;
|
||||||
|
using VisionBuilder.UI.RaspberryAcquisition.Models;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition.Services;
|
||||||
|
|
||||||
|
public class ConfigurableLibcameraSource : IImageSource, IDisposable
|
||||||
|
{
|
||||||
|
private static readonly byte[] JpegHeader = [0xff, 0xd8];
|
||||||
|
private static readonly byte[] JpegFooter = [0xff, 0xd9];
|
||||||
|
private const int ChunkSize = 1024;
|
||||||
|
|
||||||
|
private readonly Channel<Mat> _imageChannel = Channel.CreateBounded<Mat>(
|
||||||
|
new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest });
|
||||||
|
|
||||||
|
private CaptureSettings _settings;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private Thread? _captureThread;
|
||||||
|
|
||||||
|
public int FrameCount { get; private set; }
|
||||||
|
public event Action<string>? StatusChanged;
|
||||||
|
|
||||||
|
public ConfigurableLibcameraSource(CaptureSettings settings)
|
||||||
|
{
|
||||||
|
_settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Mat> GetImage(CancellationToken token)
|
||||||
|
{
|
||||||
|
return _imageChannel.Reader.ReadAsync(token).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
FrameCount = 0;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_captureThread = new Thread(CaptureLoop) { IsBackground = true };
|
||||||
|
_captureThread.Start();
|
||||||
|
StatusChanged?.Invoke("Running");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
_captureThread?.Join(2000);
|
||||||
|
_cts?.Dispose();
|
||||||
|
_cts = null;
|
||||||
|
_captureThread = null;
|
||||||
|
StatusChanged?.Invoke("Stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSettings(CaptureSettings newSettings)
|
||||||
|
{
|
||||||
|
_settings = newSettings;
|
||||||
|
Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaptureLoop()
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "rpicam-vid",
|
||||||
|
Arguments = _settings.BuildArguments(),
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
|
||||||
|
Process? process = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process = Process.Start(psi);
|
||||||
|
if (process == null)
|
||||||
|
{
|
||||||
|
StatusChanged?.Invoke("Error: failed to start rpicam-vid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var br = new BinaryReader(process.StandardOutput.BaseStream);
|
||||||
|
var imageBuffer = new byte[1024 * 1024];
|
||||||
|
var buff = br.ReadBytes(ChunkSize);
|
||||||
|
|
||||||
|
while (!_cts!.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var imageStart = Find(buff, JpegHeader);
|
||||||
|
if (imageStart == -1)
|
||||||
|
{
|
||||||
|
StatusChanged?.Invoke("Error: JPEG header not found");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = buff.Length - imageStart;
|
||||||
|
Array.Copy(buff, imageStart, imageBuffer, 0, size);
|
||||||
|
|
||||||
|
while (!_cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
buff = br.ReadBytes(ChunkSize);
|
||||||
|
var imageEnd = Find(buff, JpegFooter);
|
||||||
|
if (imageEnd != -1)
|
||||||
|
{
|
||||||
|
imageEnd += JpegFooter.Length;
|
||||||
|
var frame = new byte[size + imageEnd];
|
||||||
|
Array.Copy(imageBuffer, frame, size);
|
||||||
|
Array.Copy(buff, 0, frame, size, imageEnd);
|
||||||
|
|
||||||
|
var decoded = Mat.ImDecode(frame);
|
||||||
|
FrameCount++;
|
||||||
|
_imageChannel.Writer.TryWrite(decoded);
|
||||||
|
|
||||||
|
var remaining = buff.Length - imageEnd;
|
||||||
|
var newBuff = new byte[ChunkSize];
|
||||||
|
Array.Copy(buff, imageEnd, newBuff, 0, remaining);
|
||||||
|
var temp = br.ReadBytes(imageEnd);
|
||||||
|
Array.Copy(temp, 0, newBuff, remaining, temp.Length);
|
||||||
|
buff = newBuff;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Copy(buff, 0, imageBuffer, size, buff.Length);
|
||||||
|
size += buff.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
StatusChanged?.Invoke($"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (process is { HasExited: false })
|
||||||
|
{
|
||||||
|
process.Kill();
|
||||||
|
process.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Find(byte[] buff, byte[] search)
|
||||||
|
{
|
||||||
|
for (int start = 0; start <= buff.Length - search.Length; start++)
|
||||||
|
{
|
||||||
|
if (buff[start] == search[0])
|
||||||
|
{
|
||||||
|
int next;
|
||||||
|
for (next = 1; next < search.Length; next++)
|
||||||
|
{
|
||||||
|
if (buff[start + next] != search[next])
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (next == search.Length)
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using OpenCvSharp;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition.Services;
|
||||||
|
|
||||||
|
public static class ImageConverter
|
||||||
|
{
|
||||||
|
public static Avalonia.Media.Imaging.Bitmap MatToAvaloniaBitmap(Mat mat)
|
||||||
|
{
|
||||||
|
var encoded = mat.ImEncode(".bmp");
|
||||||
|
using var ms = new MemoryStream(encoded);
|
||||||
|
return new Avalonia.Media.Imaging.Bitmap(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using OpenCvSharp;
|
||||||
|
using VisionBuilder.UI.RaspberryAcquisition.Models;
|
||||||
|
using VisionBuilder.UI.RaspberryAcquisition.Services;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition.ViewModels;
|
||||||
|
|
||||||
|
public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||||
|
{
|
||||||
|
private const string CalibrationFile = "calibration.json";
|
||||||
|
|
||||||
|
private ConfigurableLibcameraSource? _source;
|
||||||
|
private CancellationTokenSource? _previewCts;
|
||||||
|
private Mat? _lastFrame;
|
||||||
|
private Mat? _cameraMatrix;
|
||||||
|
private Mat? _distCoeffs;
|
||||||
|
|
||||||
|
[ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage;
|
||||||
|
[ObservableProperty] private string _statusText = "Stopped";
|
||||||
|
[ObservableProperty] private int _frameCount;
|
||||||
|
[ObservableProperty] private bool _isRunning;
|
||||||
|
[ObservableProperty] private bool _isCalibrated;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _width = "2028";
|
||||||
|
[ObservableProperty] private string _height = "1520";
|
||||||
|
[ObservableProperty] private string _shutterSpeed = "10000";
|
||||||
|
[ObservableProperty] private string _framerate = "2";
|
||||||
|
[ObservableProperty] private string _awbGainRed = "3.86";
|
||||||
|
[ObservableProperty] private string _awbGainBlue = "1.46";
|
||||||
|
[ObservableProperty] private string _saveDirectory = "/home/oem/captures";
|
||||||
|
[ObservableProperty] private string _calibrationDirectory = "/home/oem/captures";
|
||||||
|
|
||||||
|
public MainWindowViewModel()
|
||||||
|
{
|
||||||
|
TryLoadCalibration();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryLoadCalibration()
|
||||||
|
{
|
||||||
|
if (!File.Exists(CalibrationFile)) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(_cameraMatrix, _distCoeffs) = CameraCalibrationService.LoadCalibration(CalibrationFile);
|
||||||
|
IsCalibrated = true;
|
||||||
|
StatusText = "Calibration loaded";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusText = $"Calibration load error: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void StartCapture()
|
||||||
|
{
|
||||||
|
if (IsRunning) return;
|
||||||
|
|
||||||
|
var settings = BuildSettings();
|
||||||
|
if (settings == null) return;
|
||||||
|
|
||||||
|
_source = new ConfigurableLibcameraSource(settings);
|
||||||
|
_source.StatusChanged += status =>
|
||||||
|
Dispatcher.UIThread.Post(() => StatusText = status);
|
||||||
|
|
||||||
|
_source.Start();
|
||||||
|
IsRunning = true;
|
||||||
|
|
||||||
|
_previewCts = new CancellationTokenSource();
|
||||||
|
_ = PreviewLoopAsync(_previewCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void StopCapture()
|
||||||
|
{
|
||||||
|
if (!IsRunning) return;
|
||||||
|
|
||||||
|
_previewCts?.Cancel();
|
||||||
|
_source?.Stop();
|
||||||
|
_source?.Dispose();
|
||||||
|
_source = null;
|
||||||
|
IsRunning = false;
|
||||||
|
StatusText = "Stopped";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ApplySettings()
|
||||||
|
{
|
||||||
|
if (!IsRunning || _source == null) return;
|
||||||
|
|
||||||
|
var settings = BuildSettings();
|
||||||
|
if (settings == null) return;
|
||||||
|
|
||||||
|
_source.UpdateSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void SaveImage()
|
||||||
|
{
|
||||||
|
if (_lastFrame == null || _lastFrame.Empty()) return;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(SaveDirectory);
|
||||||
|
var filename = $"capture_{DateTime.Now:yyyyMMdd_HHmmss}.png";
|
||||||
|
var path = Path.Combine(SaveDirectory, filename);
|
||||||
|
Cv2.ImWrite(path, _lastFrame);
|
||||||
|
StatusText = $"Saved: {filename}";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CalibrateCamera()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StatusText = "Calibrating...";
|
||||||
|
var data = CameraCalibrationService.Calibrate(CalibrationDirectory, new Size(9, 6));
|
||||||
|
CameraCalibrationService.SaveCalibration(data, CalibrationFile);
|
||||||
|
|
||||||
|
_cameraMatrix?.Dispose();
|
||||||
|
_distCoeffs?.Dispose();
|
||||||
|
(_cameraMatrix, _distCoeffs) = CameraCalibrationService.LoadCalibration(CalibrationFile);
|
||||||
|
|
||||||
|
IsCalibrated = true;
|
||||||
|
StatusText = $"Calibrated (RMS: {data.RmsError:F4})";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusText = $"Calibration error: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PreviewLoopAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested && _source != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var frame = await _source.GetImage(ct);
|
||||||
|
|
||||||
|
if (_cameraMatrix != null && _distCoeffs != null)
|
||||||
|
{
|
||||||
|
var corrected = new Mat();
|
||||||
|
Cv2.Undistort(frame, corrected, _cameraMatrix, _distCoeffs);
|
||||||
|
frame.Dispose();
|
||||||
|
frame = corrected;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastFrame?.Dispose();
|
||||||
|
_lastFrame = frame;
|
||||||
|
|
||||||
|
var bitmap = ImageConverter.MatToAvaloniaBitmap(frame);
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
var old = PreviewImage;
|
||||||
|
PreviewImage = bitmap;
|
||||||
|
FrameCount = _source?.FrameCount ?? 0;
|
||||||
|
old?.Dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
StatusText = $"Preview error: {ex.Message}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CaptureSettings? BuildSettings()
|
||||||
|
{
|
||||||
|
if (!int.TryParse(Width, out var w) ||
|
||||||
|
!int.TryParse(Height, out var h) ||
|
||||||
|
!int.TryParse(ShutterSpeed, out var s) ||
|
||||||
|
!int.TryParse(Framerate, out var f) ||
|
||||||
|
!double.TryParse(AwbGainRed, CultureInfo.InvariantCulture, out var r) ||
|
||||||
|
!double.TryParse(AwbGainBlue, CultureInfo.InvariantCulture, out var b))
|
||||||
|
{
|
||||||
|
StatusText = "Error: invalid settings values";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CaptureSettings
|
||||||
|
{
|
||||||
|
Width = w,
|
||||||
|
Height = h,
|
||||||
|
ShutterSpeed = s,
|
||||||
|
Framerate = f,
|
||||||
|
AwbGainRed = r,
|
||||||
|
AwbGainBlue = b
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
StopCapture();
|
||||||
|
_lastFrame?.Dispose();
|
||||||
|
_cameraMatrix?.Dispose();
|
||||||
|
_distCoeffs?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
78
VisionBuilder.UI.RaspberryAcquisition/Views/MainWindow.axaml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:VisionBuilder.UI.RaspberryAcquisition.ViewModels"
|
||||||
|
x:Class="VisionBuilder.UI.RaspberryAcquisition.Views.MainWindow"
|
||||||
|
x:DataType="vm:MainWindowViewModel"
|
||||||
|
Title="RPi Image Acquisition"
|
||||||
|
Width="1024" Height="800">
|
||||||
|
|
||||||
|
<DockPanel>
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="8">
|
||||||
|
<Button Content="Start" Command="{Binding StartCaptureCommand}" IsEnabled="{Binding !IsRunning}" />
|
||||||
|
<Button Content="Stop" Command="{Binding StopCaptureCommand}" IsEnabled="{Binding IsRunning}" />
|
||||||
|
<Button Content="Save Image" Command="{Binding SaveImageCommand}" />
|
||||||
|
<Button Content="Apply Settings" Command="{Binding ApplySettingsCommand}" IsEnabled="{Binding IsRunning}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<Border DockPanel.Dock="Bottom" Background="#1E1E1E" Padding="8,4">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<TextBlock Text="{Binding StatusText}" Foreground="LightGray" />
|
||||||
|
<TextBlock Foreground="LightGray">
|
||||||
|
<TextBlock.Text>
|
||||||
|
<MultiBinding StringFormat="Frames: {0}">
|
||||||
|
<Binding Path="FrameCount" />
|
||||||
|
</MultiBinding>
|
||||||
|
</TextBlock.Text>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Settings Panel -->
|
||||||
|
<Border DockPanel.Dock="Right" Width="220" Padding="12" Background="#2D2D2D">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Camera Settings" FontWeight="Bold" FontSize="14" Foreground="White" />
|
||||||
|
|
||||||
|
<TextBlock Text="Width" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding Width}" />
|
||||||
|
|
||||||
|
<TextBlock Text="Height" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding Height}" />
|
||||||
|
|
||||||
|
<TextBlock Text="Shutter (µs)" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding ShutterSpeed}" />
|
||||||
|
|
||||||
|
<TextBlock Text="Framerate" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding Framerate}" />
|
||||||
|
|
||||||
|
<TextBlock Text="AWB Gain Red" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding AwbGainRed}" />
|
||||||
|
|
||||||
|
<TextBlock Text="AWB Gain Blue" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding AwbGainBlue}" />
|
||||||
|
|
||||||
|
<Separator Margin="0,8" />
|
||||||
|
|
||||||
|
<TextBlock Text="Save Directory" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding SaveDirectory}" />
|
||||||
|
|
||||||
|
<Separator Margin="0,8" />
|
||||||
|
|
||||||
|
<TextBlock Text="Calibration" FontWeight="Bold" FontSize="14" Foreground="White" />
|
||||||
|
<TextBlock Text="Calibration Dir" Foreground="LightGray" />
|
||||||
|
<TextBox Text="{Binding CalibrationDirectory}" />
|
||||||
|
<Button Content="Calibrate" Command="{Binding CalibrateCameraCommand}" Margin="0,4" />
|
||||||
|
<TextBlock Text="Calibrated" Foreground="LimeGreen" IsVisible="{Binding IsCalibrated}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Live Preview -->
|
||||||
|
<Border Background="#1A1A1A" Margin="4">
|
||||||
|
<Image Source="{Binding PreviewImage}" Stretch="Uniform" />
|
||||||
|
</Border>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.RaspberryAcquisition.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<RuntimeIdentifiers>linux-arm64</RuntimeIdentifiers>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.LinuxFramebuffer" Version="11.2.3" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
|
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
|
||||||
|
<PackageReference Include="swety.OpenCvSharp4.runtime.linux-arm64" Version="4.10.0.20241108" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -67,8 +67,8 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe
|
|||||||
|
|
||||||
protected override void Initialize(RecipeData currentRecipe)
|
protected override void Initialize(RecipeData currentRecipe)
|
||||||
{
|
{
|
||||||
var path = "..\\Data\\Recipes\\" + currentRecipe.RecipeName + ".jhrcp";
|
var path = Path.GetFullPath(Path.Combine("..", "Data", "Recipes", currentRecipe.RecipeName + ".jhrcp"));
|
||||||
_currentRecipeFilePath = Path.GetFullPath(path);
|
_currentRecipeFilePath = path;
|
||||||
_workflow = WorkflowList.LoadJSONFromFile(path);
|
_workflow = WorkflowList.LoadJSONFromFile(path);
|
||||||
StartFileWatcher(_currentRecipeFilePath);
|
StartFileWatcher(_currentRecipeFilePath);
|
||||||
}
|
}
|
||||||
|
|||||||
170
VisionBuilder.UI.Tests/DynamicButtonTests.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||||
|
|
||||||
|
namespace VisionBuilder.UI.Tests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class DynamicButtonTests
|
||||||
|
{
|
||||||
|
private class StubSettingsService : ISettingsService
|
||||||
|
{
|
||||||
|
public Task ShowSettingsDialogAsync() => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region ButtonDefinition Tests
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ClickButton_Execute_InvokesCallback()
|
||||||
|
{
|
||||||
|
bool called = false;
|
||||||
|
var button = new ButtonDefinition("key1", "Test", () => called = true);
|
||||||
|
|
||||||
|
button.Execute();
|
||||||
|
|
||||||
|
Assert.IsTrue(called);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ClickButton_HasCorrectProperties()
|
||||||
|
{
|
||||||
|
var button = new ButtonDefinition("myKey", "My Title", () => { });
|
||||||
|
|
||||||
|
Assert.AreEqual("myKey", button.Key);
|
||||||
|
Assert.AreEqual("My Title", button.Title);
|
||||||
|
Assert.AreEqual(EButtonType.Click, button.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToggleButton_Execute_TogglesAndInvokesCallback()
|
||||||
|
{
|
||||||
|
bool? receivedState = null;
|
||||||
|
var button = new ButtonDefinition("key1", "Test", (state) => receivedState = state);
|
||||||
|
|
||||||
|
button.Execute();
|
||||||
|
|
||||||
|
Assert.IsTrue(button.IsToggled);
|
||||||
|
Assert.AreEqual(true, receivedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToggleButton_ExecuteTwice_TogglesBack()
|
||||||
|
{
|
||||||
|
bool? receivedState = null;
|
||||||
|
var button = new ButtonDefinition("key1", "Test", (state) => receivedState = state);
|
||||||
|
|
||||||
|
button.Execute();
|
||||||
|
button.Execute();
|
||||||
|
|
||||||
|
Assert.IsFalse(button.IsToggled);
|
||||||
|
Assert.AreEqual(false, receivedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToggleButton_InitialState_IsRespected()
|
||||||
|
{
|
||||||
|
var button = new ButtonDefinition("key1", "Test", (_) => { }, initialState: true);
|
||||||
|
|
||||||
|
Assert.IsTrue(button.IsToggled);
|
||||||
|
Assert.AreEqual(EButtonType.Toggle, button.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ButtonDefinition_IsVisible_DefaultTrue()
|
||||||
|
{
|
||||||
|
var button = new ButtonDefinition("key1", "Test", () => { });
|
||||||
|
|
||||||
|
Assert.IsTrue(button.IsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ButtonDefinition_SetVisibility_RaisesPropertyChanged()
|
||||||
|
{
|
||||||
|
var button = new ButtonDefinition("key1", "Test", () => { });
|
||||||
|
string? changedProperty = null;
|
||||||
|
button.PropertyChanged += (_, args) => changedProperty = args.PropertyName;
|
||||||
|
|
||||||
|
button.IsVisible = false;
|
||||||
|
|
||||||
|
Assert.AreEqual(nameof(ButtonDefinition.IsVisible), changedProperty);
|
||||||
|
Assert.IsFalse(button.IsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToggleButton_Execute_RaisesToggledPropertyChanged()
|
||||||
|
{
|
||||||
|
var button = new ButtonDefinition("key1", "Test", (_) => { });
|
||||||
|
string? changedProperty = null;
|
||||||
|
button.PropertyChanged += (_, args) => changedProperty = args.PropertyName;
|
||||||
|
|
||||||
|
button.Execute();
|
||||||
|
|
||||||
|
Assert.AreEqual(nameof(ButtonDefinition.IsToggled), changedProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region MainWindowVM Tests
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void MainWindowVM_AddButton_AddsToCollection()
|
||||||
|
{
|
||||||
|
var vm = new MainWindowVM(new StubSettingsService());
|
||||||
|
var button = new ButtonDefinition("key1", "Test", () => { });
|
||||||
|
|
||||||
|
vm.AddButton(button);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, vm.DynamicButtons.Count);
|
||||||
|
Assert.AreSame(button, vm.DynamicButtons[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void MainWindowVM_SetButtonVisibility_UpdatesButton()
|
||||||
|
{
|
||||||
|
var vm = new MainWindowVM(new StubSettingsService());
|
||||||
|
var button = new ButtonDefinition("key1", "Test", () => { });
|
||||||
|
vm.AddButton(button);
|
||||||
|
|
||||||
|
vm.SetButtonVisibility("key1", false);
|
||||||
|
|
||||||
|
Assert.IsFalse(button.IsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void MainWindowVM_SetButtonVisibility_NonExistentKey_DoesNothing()
|
||||||
|
{
|
||||||
|
var vm = new MainWindowVM(new StubSettingsService());
|
||||||
|
var button = new ButtonDefinition("key1", "Test", () => { });
|
||||||
|
vm.AddButton(button);
|
||||||
|
|
||||||
|
vm.SetButtonVisibility("nonexistent", false);
|
||||||
|
|
||||||
|
Assert.IsTrue(button.IsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void MainWindowVM_RemoveButton_RemovesFromCollection()
|
||||||
|
{
|
||||||
|
var vm = new MainWindowVM(new StubSettingsService());
|
||||||
|
var button = new ButtonDefinition("key1", "Test", () => { });
|
||||||
|
vm.AddButton(button);
|
||||||
|
|
||||||
|
vm.RemoveButton("key1");
|
||||||
|
|
||||||
|
Assert.AreEqual(0, vm.DynamicButtons.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void MainWindowVM_RemoveButton_NonExistentKey_DoesNothing()
|
||||||
|
{
|
||||||
|
var vm = new MainWindowVM(new StubSettingsService());
|
||||||
|
var button = new ButtonDefinition("key1", "Test", () => { });
|
||||||
|
vm.AddButton(button);
|
||||||
|
|
||||||
|
vm.RemoveButton("nonexistent");
|
||||||
|
|
||||||
|
Assert.AreEqual(1, vm.DynamicButtons.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
using MaterialSkin;
|
using MaterialSkin;
|
||||||
using MaterialSkin.Controls;
|
using MaterialSkin.Controls;
|
||||||
using VisionBuilder.UI.Common;
|
using VisionBuilder.UI.Common;
|
||||||
using VisionBuilder.UI.Common.Services;
|
using VisionBuilder.UI.Common.Services;
|
||||||
using VisionBuilder.UI.Common.ViewModel;
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
|
||||||
namespace VisionBuilder.UI.Windows.Duo
|
namespace VisionBuilder.UI.Windows.Duo
|
||||||
{
|
{
|
||||||
@@ -12,10 +14,11 @@ namespace VisionBuilder.UI.Windows.Duo
|
|||||||
private readonly IPasswordInputService _passwordInputService;
|
private readonly IPasswordInputService _passwordInputService;
|
||||||
private readonly UIConfiguration _uiConfiguration;
|
private readonly UIConfiguration _uiConfiguration;
|
||||||
private readonly WindowsUISettings _windowsUiSettings;
|
private readonly WindowsUISettings _windowsUiSettings;
|
||||||
|
private readonly Dictionary<string, Control> _dynamicButtonControls = new();
|
||||||
|
|
||||||
public Form1(MainWindowVM mainWindowVm, IPasswordInputService passwordInputService, UIConfiguration uiConfiguration, WindowsUISettings windowsUiSettings)
|
public Form1(MainWindowVM mainWindowVm, IPasswordInputService passwordInputService, UIConfiguration uiConfiguration, WindowsUISettings windowsUiSettings)
|
||||||
{
|
{
|
||||||
|
|
||||||
_mainWindowVm = mainWindowVm;
|
_mainWindowVm = mainWindowVm;
|
||||||
_passwordInputService = passwordInputService;
|
_passwordInputService = passwordInputService;
|
||||||
_uiConfiguration = uiConfiguration;
|
_uiConfiguration = uiConfiguration;
|
||||||
@@ -30,6 +33,9 @@ namespace VisionBuilder.UI.Windows.Duo
|
|||||||
if(!_uiConfiguration.ShowTestModeButton)
|
if(!_uiConfiguration.ShowTestModeButton)
|
||||||
flowLayoutPanel1.Controls.Remove(btnTestMode);
|
flowLayoutPanel1.Controls.Remove(btnTestMode);
|
||||||
|
|
||||||
|
_mainWindowVm.DynamicButtons.CollectionChanged += DynamicButtons_CollectionChanged;
|
||||||
|
foreach (var btn in _mainWindowVm.DynamicButtons)
|
||||||
|
AddDynamicButton(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _mainWindowVm_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void _mainWindowVm_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
@@ -105,5 +111,69 @@ namespace VisionBuilder.UI.Windows.Duo
|
|||||||
{
|
{
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DynamicButtons_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (ButtonDefinition btn in e.NewItems!)
|
||||||
|
AddDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
foreach (ButtonDefinition btn in e.OldItems!)
|
||||||
|
RemoveDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Reset:
|
||||||
|
foreach (var ctrl in _dynamicButtonControls.Values)
|
||||||
|
flowLayoutPanel1.Controls.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
var btn = new MaterialRaisedButton
|
||||||
|
{
|
||||||
|
Text = buttonDef.Title,
|
||||||
|
Size = new Size(176, 64),
|
||||||
|
Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold),
|
||||||
|
DrawBorder = true,
|
||||||
|
Primary = buttonDef.Type == EButtonType.Toggle && buttonDef.IsToggled,
|
||||||
|
Visible = buttonDef.IsVisible
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
buttonDef.Execute();
|
||||||
|
if (buttonDef.Type == EButtonType.Toggle)
|
||||||
|
btn.Primary = buttonDef.IsToggled;
|
||||||
|
};
|
||||||
|
|
||||||
|
buttonDef.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||||
|
btn.Visible = buttonDef.IsVisible;
|
||||||
|
else if (args.PropertyName == nameof(ButtonDefinition.IsToggled))
|
||||||
|
btn.Primary = buttonDef.IsToggled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert before Minimize button
|
||||||
|
var minimizeIndex = flowLayoutPanel1.Controls.IndexOf(btnMinimize);
|
||||||
|
if (minimizeIndex >= 0)
|
||||||
|
flowLayoutPanel1.Controls.Add(btn);
|
||||||
|
flowLayoutPanel1.Controls.SetChildIndex(btn, minimizeIndex >= 0 ? minimizeIndex : flowLayoutPanel1.Controls.Count - 1);
|
||||||
|
_dynamicButtonControls[buttonDef.Key] = btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
if (_dynamicButtonControls.TryGetValue(buttonDef.Key, out var ctrl))
|
||||||
|
{
|
||||||
|
flowLayoutPanel1.Controls.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Remove(buttonDef.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using MaterialSkin;
|
using MaterialSkin;
|
||||||
using MaterialSkin.Controls;
|
using MaterialSkin.Controls;
|
||||||
@@ -6,6 +7,7 @@ using OpenCvSharp;
|
|||||||
using VisionBuilder.UI.Common;
|
using VisionBuilder.UI.Common;
|
||||||
using VisionBuilder.UI.Common.Services;
|
using VisionBuilder.UI.Common.Services;
|
||||||
using VisionBuilder.UI.Common.ViewModel;
|
using VisionBuilder.UI.Common.ViewModel;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
|
||||||
namespace VisionBuilder.UI.Windows.Test
|
namespace VisionBuilder.UI.Windows.Test
|
||||||
{
|
{
|
||||||
@@ -15,10 +17,11 @@ namespace VisionBuilder.UI.Windows.Test
|
|||||||
private readonly IPasswordInputService _passwordInputService;
|
private readonly IPasswordInputService _passwordInputService;
|
||||||
private readonly UIConfiguration _uiConfiguration;
|
private readonly UIConfiguration _uiConfiguration;
|
||||||
private readonly WindowsUISettings _windowsUiSettings;
|
private readonly WindowsUISettings _windowsUiSettings;
|
||||||
|
private readonly Dictionary<string, Control> _dynamicButtonControls = new();
|
||||||
|
|
||||||
public Form1(MainWindowVM mainWindowVm, IPasswordInputService passwordInputService, UIConfiguration uiConfiguration, WindowsUISettings windowsUiSettings)
|
public Form1(MainWindowVM mainWindowVm, IPasswordInputService passwordInputService, UIConfiguration uiConfiguration, WindowsUISettings windowsUiSettings)
|
||||||
{
|
{
|
||||||
|
|
||||||
_mainWindowVm = mainWindowVm;
|
_mainWindowVm = mainWindowVm;
|
||||||
_passwordInputService = passwordInputService;
|
_passwordInputService = passwordInputService;
|
||||||
_uiConfiguration = uiConfiguration;
|
_uiConfiguration = uiConfiguration;
|
||||||
@@ -32,6 +35,9 @@ namespace VisionBuilder.UI.Windows.Test
|
|||||||
if(!_uiConfiguration.ShowTestModeButton)
|
if(!_uiConfiguration.ShowTestModeButton)
|
||||||
flowLayoutPanel1.Controls.Remove(btnTestMode);
|
flowLayoutPanel1.Controls.Remove(btnTestMode);
|
||||||
|
|
||||||
|
_mainWindowVm.DynamicButtons.CollectionChanged += DynamicButtons_CollectionChanged;
|
||||||
|
foreach (var btn in _mainWindowVm.DynamicButtons)
|
||||||
|
AddDynamicButton(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _mainWindowVm_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void _mainWindowVm_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
@@ -107,5 +113,69 @@ namespace VisionBuilder.UI.Windows.Test
|
|||||||
{
|
{
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DynamicButtons_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (ButtonDefinition btn in e.NewItems!)
|
||||||
|
AddDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
foreach (ButtonDefinition btn in e.OldItems!)
|
||||||
|
RemoveDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Reset:
|
||||||
|
foreach (var ctrl in _dynamicButtonControls.Values)
|
||||||
|
flowLayoutPanel1.Controls.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
var btn = new MaterialRaisedButton
|
||||||
|
{
|
||||||
|
Text = buttonDef.Title,
|
||||||
|
Size = new Size(176, 64),
|
||||||
|
Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold),
|
||||||
|
DrawBorder = true,
|
||||||
|
Primary = buttonDef.Type == ButtonType.Toggle && buttonDef.IsToggled,
|
||||||
|
Visible = buttonDef.IsVisible
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
buttonDef.Execute();
|
||||||
|
if (buttonDef.Type == ButtonType.Toggle)
|
||||||
|
btn.Primary = buttonDef.IsToggled;
|
||||||
|
};
|
||||||
|
|
||||||
|
buttonDef.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||||
|
btn.Visible = buttonDef.IsVisible;
|
||||||
|
else if (args.PropertyName == nameof(ButtonDefinition.IsToggled))
|
||||||
|
btn.Primary = buttonDef.IsToggled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert before Minimize button
|
||||||
|
var minimizeIndex = flowLayoutPanel1.Controls.IndexOf(btnMinimize);
|
||||||
|
if (minimizeIndex >= 0)
|
||||||
|
flowLayoutPanel1.Controls.Add(btn);
|
||||||
|
flowLayoutPanel1.Controls.SetChildIndex(btn, minimizeIndex >= 0 ? minimizeIndex : flowLayoutPanel1.Controls.Count - 1);
|
||||||
|
_dynamicButtonControls[buttonDef.Key] = btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
if (_dynamicButtonControls.TryGetValue(buttonDef.Key, out var ctrl))
|
||||||
|
{
|
||||||
|
flowLayoutPanel1.Controls.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Remove(buttonDef.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
@@ -8,12 +9,14 @@ using System.Text;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using VisionBuilder.UI.Common;
|
using VisionBuilder.UI.Common;
|
||||||
|
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||||
|
|
||||||
namespace VisionBuilder.UI.Windows.Components
|
namespace VisionBuilder.UI.Windows.Components
|
||||||
{
|
{
|
||||||
public partial class SingleCameraControl : UserControl
|
public partial class SingleCameraControl : UserControl
|
||||||
{
|
{
|
||||||
private SingleCameraVM _singleCameraVm;
|
private SingleCameraVM _singleCameraVm;
|
||||||
|
private readonly Dictionary<string, Control> _dynamicButtonControls = new();
|
||||||
|
|
||||||
public void SetViewModel(SingleCameraVM singleCameraVm)
|
public void SetViewModel(SingleCameraVM singleCameraVm)
|
||||||
{
|
{
|
||||||
@@ -25,11 +28,15 @@ namespace VisionBuilder.UI.Windows.Components
|
|||||||
|
|
||||||
SetBindings();
|
SetBindings();
|
||||||
_singleCameraVm.PropertyChanged += SingleCameraVm_PropertyChanged;
|
_singleCameraVm.PropertyChanged += SingleCameraVm_PropertyChanged;
|
||||||
|
|
||||||
|
_singleCameraVm.DynamicButtons.CollectionChanged += DynamicButtons_CollectionChanged;
|
||||||
|
foreach (var btn in _singleCameraVm.DynamicButtons)
|
||||||
|
AddDynamicButton(btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SingleCameraVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void SingleCameraVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetBindings()
|
private void SetBindings()
|
||||||
@@ -53,9 +60,69 @@ namespace VisionBuilder.UI.Windows.Components
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DynamicButtons_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (ButtonDefinition btn in e.NewItems!)
|
||||||
|
AddDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
foreach (ButtonDefinition btn in e.OldItems!)
|
||||||
|
RemoveDynamicButton(btn);
|
||||||
|
break;
|
||||||
|
case NotifyCollectionChangedAction.Reset:
|
||||||
|
foreach (var ctrl in _dynamicButtonControls.Values)
|
||||||
|
flowLayoutPanel2.Controls.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
var btn = new MaterialSkin.Controls.MaterialRaisedButton
|
||||||
|
{
|
||||||
|
Text = buttonDef.Title,
|
||||||
|
Size = new Size(201, 85),
|
||||||
|
Font = new Font("Segoe UI", 9F, FontStyle.Bold),
|
||||||
|
DrawBorder = true,
|
||||||
|
Primary = buttonDef.Type == EButtonType.Toggle && buttonDef.IsToggled,
|
||||||
|
Visible = buttonDef.IsVisible
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
buttonDef.Execute();
|
||||||
|
if (buttonDef.Type == EButtonType.Toggle)
|
||||||
|
btn.Primary = buttonDef.IsToggled;
|
||||||
|
};
|
||||||
|
|
||||||
|
buttonDef.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||||
|
btn.Visible = buttonDef.IsVisible;
|
||||||
|
else if (args.PropertyName == nameof(ButtonDefinition.IsToggled))
|
||||||
|
btn.Primary = buttonDef.IsToggled;
|
||||||
|
};
|
||||||
|
|
||||||
|
flowLayoutPanel2.Controls.Add(btn);
|
||||||
|
_dynamicButtonControls[buttonDef.Key] = btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDynamicButton(ButtonDefinition buttonDef)
|
||||||
|
{
|
||||||
|
if (_dynamicButtonControls.TryGetValue(buttonDef.Key, out var ctrl))
|
||||||
|
{
|
||||||
|
flowLayoutPanel2.Controls.Remove(ctrl);
|
||||||
|
_dynamicButtonControls.Remove(buttonDef.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public SingleCameraControl()
|
public SingleCameraControl()
|
||||||
{
|
{
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 18.3.11520.95 d18.3
|
VisualStudioVersion = 18.3.11520.95
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Common", "VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj", "{5AFF312A-EF5F-49ED-BF54-5AC69F7BEC03}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Common", "VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj", "{5AFF312A-EF5F-49ED-BF54-5AC69F7BEC03}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -108,6 +108,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Blazor.Uno
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera", "Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera\Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera.csproj", "{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera", "Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera\Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera.csproj", "{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.RaspberryAcquisition", "VisionBuilder.UI.RaspberryAcquisition\VisionBuilder.UI.RaspberryAcquisition.csproj", "{149E2AFC-A714-4645-883A-2EE685E63FB9}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Avalonia.Uno", "VisionBuilder.UI.Avalonia.Uno\VisionBuilder.UI.Avalonia.Uno.csproj", "{F70920C6-EF98-42B7-9F21-6E94781E9900}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LindtLeerformPlugin", "Plugins\LindtLeerformPlugin\LindtLeerformPlugin.csproj", "{5A1D9E37-353C-4E30-A302-48487DC3A9E8}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -584,6 +590,42 @@ Global
|
|||||||
{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE}.Release|x64.Build.0 = Release|Any CPU
|
{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE}.Release|x86.ActiveCfg = Release|Any CPU
|
{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE}.Release|x86.Build.0 = Release|Any CPU
|
{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -623,6 +665,9 @@ Global
|
|||||||
{0C14CF80-D743-4809-9B9C-2A096FEA2E06} = {FA674B5A-3394-926C-2B1E-70E5B00E4A5C}
|
{0C14CF80-D743-4809-9B9C-2A096FEA2E06} = {FA674B5A-3394-926C-2B1E-70E5B00E4A5C}
|
||||||
{9AE35B63-607E-4009-9FE6-3DF2CD98071F} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
{9AE35B63-607E-4009-9FE6-3DF2CD98071F} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
||||||
{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE} = {D05689E3-04C6-4E3B-ACA7-3F4507CED4CC}
|
{FAD15887-EFB2-4FA5-8BD9-586C1DB2B0BE} = {D05689E3-04C6-4E3B-ACA7-3F4507CED4CC}
|
||||||
|
{149E2AFC-A714-4645-883A-2EE685E63FB9} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
||||||
|
{F70920C6-EF98-42B7-9F21-6E94781E9900} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
||||||
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8} = {583A77DF-A293-4F3E-AB96-7310BC495822}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677}
|
SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677}
|
||||||
|
|||||||