4 Commits

Author SHA1 Message Date
zhengxuan.zhang 7c0f9dab73 下拉检测模块列表 2026-05-06 15:28:29 +08:00
zhengxuan.zhang 3bee2898c5 Plan 用于 CNC 默认保存和加载,Tools 用于流程图配方 xpm,Data 用于执行结果和中间图像,Report 为报告预留目录 2026-05-06 14:56:07 +08:00
zhengxuan.zhang 9a8831c945 新增设置窗体 2026-05-06 14:13:19 +08:00
zhengxuan.zhang db8a37410f 将流程图按钮改为图标 2026-05-06 13:32:21 +08:00
19 changed files with 926 additions and 91 deletions
@@ -6,6 +6,7 @@ using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.Storage;
using XplorePlane.Tests.Helpers;
using XplorePlane.ViewModels;
using Xunit;
@@ -22,6 +23,7 @@ namespace XplorePlane.Tests.Pipeline
private readonly Mock<IPipelineExecutionService> _mockExecSvc;
private readonly Mock<IPipelinePersistenceService> _mockPersistSvc;
private readonly Mock<ILoggerService> _mockLogger;
private readonly Mock<IXpDataPathService> _mockDataPathService;
public PipelineEditorViewModelTests()
{
@@ -29,11 +31,19 @@ namespace XplorePlane.Tests.Pipeline
_mockExecSvc = new Mock<IPipelineExecutionService>();
_mockPersistSvc = new Mock<IPipelinePersistenceService>();
_mockLogger = new Mock<ILoggerService>();
_mockDataPathService = new Mock<IXpDataPathService>();
_mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(_mockLogger.Object);
_mockDataPathService.SetupGet(s => s.ToolsPath).Returns(Path.GetTempPath());
}
private PipelineEditorViewModel CreateVm() =>
new PipelineEditorViewModel(_mockImageSvc.Object, _mockExecSvc.Object, _mockPersistSvc.Object, new EventAggregator(), _mockLogger.Object);
new PipelineEditorViewModel(
_mockImageSvc.Object,
_mockExecSvc.Object,
_mockPersistSvc.Object,
new EventAggregator(),
_mockLogger.Object,
_mockDataPathService.Object);
// ── 6.1 AddOperatorCommand ────────────────────────────────────
@@ -1,8 +1,10 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Moq;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.Storage;
using XplorePlane.Tests.Helpers;
using Xunit;
@@ -199,5 +201,21 @@ namespace XplorePlane.Tests.Pipeline
Assert.Equal(2, result.Count);
}
[Fact]
public async Task LoadAllAsync_UsesToolsPath_WhenConstructedWithDataPathService()
{
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur" });
var mockDataPathSvc = new Mock<IXpDataPathService>();
mockDataPathSvc.SetupGet(s => s.ToolsPath).Returns(_tempDir);
var service = new PipelinePersistenceService(mockImageSvc.Object, mockDataPathSvc.Object);
await service.SaveAsync(BuildModel("P3", "Blur"), Path.Combine(_tempDir, "p3.xpm"));
var result = await service.LoadAllAsync(null);
Assert.Single(result);
Assert.Equal("P3", result[0].Name);
}
}
}
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.Storage;
using XplorePlane.Tests.Helpers;
using XplorePlane.ViewModels;
@@ -31,8 +32,16 @@ namespace XplorePlane.Tests.Pipeline
var mockExecSvc = new Mock<IPipelineExecutionService>();
var mockPersistSvc = new Mock<IPipelinePersistenceService>();
var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockLogger.Setup(l => l.ForModule<PipelineEditorViewModel>()).Returns(mockLogger.Object);
return new PipelineEditorViewModel(mockImageSvc.Object, mockExecSvc.Object, mockPersistSvc.Object, new EventAggregator(), mockLogger.Object);
mockDataPathService.SetupGet(s => s.ToolsPath).Returns(Path.GetTempPath());
return new PipelineEditorViewModel(
mockImageSvc.Object,
mockExecSvc.Object,
mockPersistSvc.Object,
new EventAggregator(),
mockLogger.Object,
mockDataPathService.Object);
}
/// <summary>
@@ -11,6 +11,7 @@ using XP.Common.Database.Interfaces;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.Storage;
using Xunit;
namespace XplorePlane.Tests.Services
@@ -294,6 +295,33 @@ namespace XplorePlane.Tests.Services
Assert.Equal(originalHash, snapshot.PipelineHash);
}
[Fact]
public async Task Constructor_WithDataPathService_WritesIntoDataInspectionResultsDirectory()
{
var mockDataPathService = new Mock<IXpDataPathService>();
var dataRoot = Path.Combine(_tempRoot, "xpdata", "Data");
mockDataPathService.SetupGet(s => s.DataPath).Returns(dataRoot);
var store = new InspectionResultStore(_dbContext, _mockLogger.Object, mockDataPathService.Object);
var run = new InspectionRunRecord
{
ProgramName = "Program-By-Service",
WorkpieceId = "Part-03",
SerialNumber = "SN-DATA"
};
await store.BeginRunAsync(run);
await store.CompleteRunAsync(run.RunId);
var manifestPath = Path.Combine(
dataRoot,
"InspectionResults",
run.ResultRootPath.Replace('/', Path.DirectorySeparatorChar),
"manifest.json");
Assert.True(File.Exists(manifestPath));
}
public void Dispose()
{
_dbContext.Dispose();
@@ -1,4 +1,4 @@
// Feature: cnc-run-execution
// Feature: cnc-run-execution
// Properties 1, 2, 12: CncEditorViewModel execution control
using System;
@@ -16,6 +16,8 @@ using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
using XplorePlane.Services.Storage;
using XplorePlane.Services;
using XplorePlane.ViewModels.Cnc;
using Xunit;
@@ -23,16 +25,20 @@ namespace XplorePlane.Tests.ViewModels
{
public class CncEditorViewModelTests
{
// ── Helpers ──────────────────────────────────────────────────────────
// 鈹€鈹€ Helpers 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
private static CncEditorViewModel CreateVm(
Mock<ICncExecutionService> mockExecSvc = null,
CncProgram initialProgram = null)
CncProgram initialProgram = null,
Mock<IPipelinePersistenceService> mockPipelinePersistenceService = null)
{
var mockCncProgramSvc = new Mock<ICncProgramService>();
var mockAppState = new Mock<IAppStateService>();
var mockLogger = new Mock<ILoggerService>();
var mockDataPathService = new Mock<IXpDataPathService>();
mockPipelinePersistenceService ??= new Mock<IPipelinePersistenceService>();
mockLogger.Setup(l => l.ForModule<CncEditorViewModel>()).Returns(mockLogger.Object);
mockDataPathService.SetupGet(s => s.PlanPath).Returns(System.IO.Path.GetTempPath());
mockExecSvc ??= new Mock<ICncExecutionService>();
@@ -44,15 +50,52 @@ namespace XplorePlane.Tests.ViewModels
DateTime.UtcNow, DateTime.UtcNow,
new List<CncNode>
{
new ReferencePointNode(Guid.NewGuid(), 0, "参考点_0", 0, 0, 0, 0, 0, 0, false, 0, 0)
new ReferencePointNode(Guid.NewGuid(), 0, "鍙傝€冪偣_0", 0, 0, 0, 0, 0, 0, false, 0, 0)
}.AsReadOnly()));
mockCncProgramSvc
.Setup(s => s.CreateNode(It.IsAny<CncNodeType>()))
.Returns((CncNodeType nodeType) => nodeType switch
{
CncNodeType.InspectionModule => new InspectionModuleNode(Guid.NewGuid(), 0, "检测模块_0", new PipelineModel()),
CncNodeType.ReferencePoint => new ReferencePointNode(Guid.NewGuid(), 0, "参考点_0", 0, 0, 0, 0, 0, 0, false, 0, 0),
_ => throw new InvalidOperationException($"Unsupported node type in test: {nodeType}")
});
mockCncProgramSvc
.Setup(s => s.InsertNode(It.IsAny<CncProgram>(), It.IsAny<int>(), It.IsAny<CncNode>()))
.Returns((CncProgram program, int afterIndex, CncNode node) =>
{
var nodes = program.Nodes.ToList();
var insertIndex = Math.Clamp(afterIndex + 1, 0, nodes.Count);
nodes.Insert(insertIndex, node with { Index = insertIndex });
for (var i = 0; i < nodes.Count; i++)
{
nodes[i] = nodes[i] with { Index = i };
}
return program with { Nodes = nodes.AsReadOnly(), UpdatedAt = DateTime.UtcNow };
});
mockCncProgramSvc
.Setup(s => s.UpdateNode(It.IsAny<CncProgram>(), It.IsAny<int>(), It.IsAny<CncNode>()))
.Returns((CncProgram program, int index, CncNode updatedNode) =>
{
var nodes = program.Nodes.ToList();
nodes[index] = updatedNode with { Index = index };
return program with { Nodes = nodes.AsReadOnly(), UpdatedAt = DateTime.UtcNow };
});
var vm = new CncEditorViewModel(
mockCncProgramSvc.Object,
mockAppState.Object,
new EventAggregator(),
mockLogger.Object,
mockExecSvc.Object);
mockExecSvc.Object,
mockDataPathService.Object,
mockPipelinePersistenceService.Object);
if (initialProgram != null)
{
@@ -78,9 +121,9 @@ namespace XplorePlane.Tests.ViewModels
return new CncProgram(Guid.NewGuid(), "TestProgram", DateTime.UtcNow, DateTime.UtcNow, nodes);
}
// ── Property 1: 运行/停止按钮状态互斥 ────────────────────────────────
// 鈹€鈹€ Property 1: 杩愯/鍋滄鎸夐挳鐘舵€佷簰鏂?鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
// Feature: cnc-run-execution, Property 1: 运行/停止按钮状态互斥
// Feature: cnc-run-execution, Property 1: 杩愯/鍋滄鎸夐挳鐘舵€佷簰鏂?
// Validates: Requirements 1.1, 1.3, 1.4
[Property(MaxTest = 100)]
public Property RunStop_Commands_AreMutuallyExclusive()
@@ -125,9 +168,9 @@ namespace XplorePlane.Tests.ViewModels
});
}
// ── Property 2: 执行完成后状态重置 ───────────────────────────────────
// 鈹€鈹€ Property 2: 鎵ц瀹屾垚鍚庣姸鎬侀噸缃?鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
// Feature: cnc-run-execution, Property 2: 执行完成后状态重置
// Feature: cnc-run-execution, Property 2: 鎵ц瀹屾垚鍚庣姸鎬侀噸缃?
// Validates: Requirements 1.7, 6.5
[Property(MaxTest = 100)]
public Property AfterExecution_IsRunningFalse_AllNodesIdle()
@@ -176,9 +219,9 @@ namespace XplorePlane.Tests.ViewModels
});
}
// ── Property 12: 执行中编辑命令全部禁用 ──────────────────────────────
// 鈹€鈹€ Property 12: 鎵ц涓紪杈戝懡浠ゅ叏閮ㄧ鐢?鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
// Feature: cnc-run-execution, Property 12: 执行中编辑命令全部禁用
// Feature: cnc-run-execution, Property 12: 鎵ц涓紪杈戝懡浠ゅ叏閮ㄧ鐢?
// Validates: Requirements 6.7
[Property(MaxTest = 100)]
public Property WhileRunning_AllEditCommands_AreDisabled()
@@ -228,5 +271,50 @@ namespace XplorePlane.Tests.ViewModels
return allDisabled;
});
}
[Fact]
public async Task InsertInspectionModuleFromPipelineFileAsync_LoadsPipelineAndInsertsNode()
{
var pipelineFile = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"{Guid.NewGuid():N}.xpm");
await System.IO.File.WriteAllTextAsync(pipelineFile, "{}");
try
{
var expectedPipeline = new PipelineModel
{
Name = "BuiltIn/ModuleA"
};
var mockPipelinePersistenceService = new Mock<IPipelinePersistenceService>();
mockPipelinePersistenceService
.Setup(s => s.LoadAsync(pipelineFile))
.ReturnsAsync(expectedPipeline);
var initialProgram = new CncProgram(
Guid.NewGuid(),
"TestProgram",
DateTime.UtcNow,
DateTime.UtcNow,
new List<CncNode>
{
new SavePositionNode(Guid.NewGuid(), 0, "保存位置_0", MotionState.Default)
}.AsReadOnly());
var vm = CreateVm(
initialProgram: initialProgram,
mockPipelinePersistenceService: mockPipelinePersistenceService);
await vm.InsertInspectionModuleFromPipelineFileAsync(pipelineFile);
var insertedNode = Assert.IsType<InspectionModuleNode>(vm.Nodes.Last().Model);
Assert.Same(expectedPipeline, insertedNode.Pipeline);
Assert.Equal("BuiltIn/ModuleA", insertedNode.Pipeline.Name);
}
finally
{
if (System.IO.File.Exists(pipelineFile))
System.IO.File.Delete(pipelineFile);
}
}
}
}
+1
View File
@@ -7,6 +7,7 @@
<appSettings>
<!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration -->
<add key="Language" value="ZhCN" />
<add key="XpData:RootPath" value="D:\XPData" />
<add key="UserManual" value="D:\HMQProject\XplorePlane_CT\Code\XplorePlane\XP.App\bin\Debug\net8.0-windows7.0\UserManual.pdf" />
<!-- Serilog日志配置 -->
+2
View File
@@ -40,6 +40,7 @@ using XplorePlane.Services.MainViewport;
using XplorePlane.Services.Matrix;
using XplorePlane.Services.Measurement;
using XplorePlane.Services.Recipe;
using XplorePlane.Services.Storage;
using XplorePlane.ViewModels;
using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views;
@@ -377,6 +378,7 @@ namespace XplorePlane
// 注册全局状态服务(单例)
containerRegistry.RegisterSingleton<IAppStateService, AppStateService>();
containerRegistry.RegisterSingleton<IXpDataPathService, XpDataPathService>();
// 注册检测配方服务(单例)
containerRegistry.RegisterSingleton<IRecipeService, RecipeService>();
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using XP.Common.Database.Interfaces;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.Storage;
namespace XplorePlane.Services.InspectionResults
{
@@ -160,6 +161,14 @@ WHERE run_id = @run_id";
"InspectionResults");
}
public InspectionResultStore(IDbContext db, ILoggerService logger, IXpDataPathService dataPathService)
: this(
db,
logger,
Path.Combine(dataPathService?.DataPath ?? throw new ArgumentNullException(nameof(dataPathService)), "InspectionResults"))
{
}
public async Task BeginRunAsync(InspectionRunRecord runRecord, InspectionAssetWriteRequest runSourceAsset = null)
{
ArgumentNullException.ThrowIfNull(runRecord);
@@ -6,6 +6,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using XplorePlane.Models;
using XplorePlane.Services.Storage;
namespace XplorePlane.Services
{
@@ -29,6 +30,11 @@ namespace XplorePlane.Services
"XplorePlane", "Pipelines");
}
public PipelinePersistenceService(IImageProcessingService imageProcessingService, IXpDataPathService dataPathService)
: this(imageProcessingService, dataPathService?.ToolsPath)
{
}
public async Task SaveAsync(PipelineModel pipeline, string filePath)
{
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
@@ -0,0 +1,23 @@
using System;
namespace XplorePlane.Services.Storage
{
public interface IXpDataPathService
{
string DefaultRootPath { get; }
string RootPath { get; }
string PlanPath { get; }
string ToolsPath { get; }
string DataPath { get; }
string ReportPath { get; }
string LegacyInspectionDataPath { get; }
void SaveRootPath(string rootPath);
}
}
@@ -0,0 +1,145 @@
using System;
using System.Configuration;
using System.IO;
using XP.Common.Logging.Interfaces;
namespace XplorePlane.Services.Storage
{
public class XpDataPathService : IXpDataPathService
{
internal const string RootPathSettingKey = "XpData:RootPath";
private const string DefaultRoot = @"D:\XPData";
private readonly ILoggerService _logger;
private readonly object _syncRoot = new();
private string _rootPath;
public XpDataPathService(ILoggerService logger)
{
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<XpDataPathService>();
_rootPath = LoadConfiguredRootPath();
EnsureManagedDirectories(_rootPath);
}
public string DefaultRootPath => DefaultRoot;
public string RootPath
{
get
{
lock (_syncRoot)
{
return _rootPath;
}
}
}
public string PlanPath => EnsureSubdirectory("Plan");
public string ToolsPath => EnsureSubdirectory("Tools");
public string DataPath => EnsureSubdirectory("Data");
public string ReportPath => EnsureSubdirectory("Report");
public string LegacyInspectionDataPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"XplorePlane",
"InspectionResults");
public void SaveRootPath(string rootPath)
{
var normalizedRootPath = NormalizeRootPath(rootPath);
EnsureManagedDirectories(normalizedRootPath);
try
{
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var appSettings = config.AppSettings.Settings;
if (appSettings[RootPathSettingKey] == null)
{
appSettings.Add(RootPathSettingKey, normalizedRootPath);
}
else
{
appSettings[RootPathSettingKey].Value = normalizedRootPath;
}
config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");
}
catch (ConfigurationErrorsException ex)
{
_logger.Error(ex, "保存 XP 数据根目录失败:配置文件写入异常");
throw new InvalidOperationException($"保存数据根目录失败:{ex.Message}", ex);
}
catch (UnauthorizedAccessException ex)
{
_logger.Error(ex, "保存 XP 数据根目录失败:无权写入配置文件");
throw new InvalidOperationException("保存数据根目录失败:没有配置文件写入权限。", ex);
}
lock (_syncRoot)
{
_rootPath = normalizedRootPath;
}
}
private string EnsureSubdirectory(string directoryName)
{
string rootPath;
lock (_syncRoot)
{
rootPath = _rootPath;
}
EnsureManagedDirectories(rootPath);
return Path.Combine(rootPath, directoryName);
}
private string LoadConfiguredRootPath()
{
try
{
return NormalizeRootPath(ConfigurationManager.AppSettings[RootPathSettingKey]);
}
catch (Exception ex)
{
_logger.Warn("读取 XP 数据根目录失败,回退默认目录:{Message}", ex.Message);
return NormalizeRootPath(null);
}
}
private static string NormalizeRootPath(string rootPath)
{
var candidate = string.IsNullOrWhiteSpace(rootPath) ? DefaultRoot : rootPath.Trim();
try
{
var normalized = Path.GetFullPath(candidate)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (!Path.IsPathRooted(normalized))
{
return DefaultRoot;
}
return normalized;
}
catch
{
return DefaultRoot;
}
}
private static void EnsureManagedDirectories(string rootPath)
{
Directory.CreateDirectory(rootPath);
Directory.CreateDirectory(Path.Combine(rootPath, "Plan"));
Directory.CreateDirectory(Path.Combine(rootPath, "Tools"));
Directory.CreateDirectory(Path.Combine(rootPath, "Data"));
Directory.CreateDirectory(Path.Combine(rootPath, "Report"));
}
}
}
@@ -16,6 +16,8 @@ using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.Cnc;
using XplorePlane.Services;
using XplorePlane.Services.Storage;
namespace XplorePlane.ViewModels.Cnc
{
@@ -28,6 +30,8 @@ namespace XplorePlane.ViewModels.Cnc
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private readonly ICncExecutionService _cncExecutionService;
private readonly IXpDataPathService _dataPathService;
private readonly IPipelinePersistenceService _pipelinePersistenceService;
private CncProgram _currentProgram;
private ObservableCollection<CncNodeViewModel> _nodes;
@@ -51,13 +55,17 @@ namespace XplorePlane.ViewModels.Cnc
IAppStateService appStateService,
IEventAggregator eventAggregator,
ILoggerService logger,
ICncExecutionService cncExecutionService)
ICncExecutionService cncExecutionService,
IXpDataPathService dataPathService,
IPipelinePersistenceService pipelinePersistenceService)
{
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
ArgumentNullException.ThrowIfNull(appStateService);
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
_cncExecutionService = cncExecutionService ?? throw new ArgumentNullException(nameof(cncExecutionService));
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
_pipelinePersistenceService = pipelinePersistenceService ?? throw new ArgumentNullException(nameof(pipelinePersistenceService));
_nodes = new ObservableCollection<CncNodeViewModel>();
_treeNodes = new ObservableCollection<CncNodeViewModel>();
@@ -331,7 +339,8 @@ namespace XplorePlane.ViewModels.Cnc
Title = "保存 CNC 程序",
Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
DefaultExt = ".xp",
FileName = _currentProgram.Name
FileName = _currentProgram.Name,
InitialDirectory = GetPlanDirectory()
};
if (dlg.ShowDialog() != true)
@@ -355,7 +364,8 @@ namespace XplorePlane.ViewModels.Cnc
{
Title = "加载 CNC 程序",
Filter = "CNC 程序文件 (*.xp)|*.xp|所有文件 (*.*)|*.*",
DefaultExt = ".xp"
DefaultExt = ".xp",
InitialDirectory = GetPlanDirectory()
};
if (dlg.ShowDialog() != true)
@@ -438,6 +448,64 @@ namespace XplorePlane.ViewModels.Cnc
}
}
public async Task InsertInspectionModuleFromPipelineFileAsync(string filePath)
{
if (IsRunning)
return;
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("检测模块文件路径不能为空。", nameof(filePath));
if (!File.Exists(filePath))
throw new FileNotFoundException("检测模块文件不存在。", filePath);
if (_currentProgram == null)
{
ExecuteNewProgram();
}
var pipeline = await _pipelinePersistenceService.LoadAsync(filePath);
try
{
var node = _cncProgramService.CreateNode(CncNodeType.InspectionModule);
if (node is not InspectionModuleNode inspectionModuleNode)
throw new InvalidOperationException("无法创建检测模块节点。");
var pipelineName = string.IsNullOrWhiteSpace(pipeline.Name)
? Path.GetFileNameWithoutExtension(filePath)
: pipeline.Name;
pipeline.Name = pipelineName;
var configuredNode = inspectionModuleNode with
{
Pipeline = pipeline,
Name = pipelineName
};
int afterIndex = ResolveInsertAfterIndex(CncNodeType.InspectionModule);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, configuredNode);
_preferredSelectedNodeId = configuredNode.Id;
ClearPendingInsertAnchor();
OnProgramEdited();
StatusMessage = $"已插入检测模块:{pipelineName}";
_logger.Info("Inserted built-in inspection module from file: {FilePath}", filePath);
}
catch (InvalidOperationException ex)
{
_logger.Warn("Built-in inspection module insertion blocked: {Message}", ex.Message);
throw;
}
}
private string GetPlanDirectory()
{
var directory = _dataPathService.PlanPath;
Directory.CreateDirectory(directory);
return directory;
}
private static string Esc(string value)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
@@ -14,6 +14,7 @@ using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.Storage;
namespace XplorePlane.ViewModels
{
@@ -28,6 +29,7 @@ namespace XplorePlane.ViewModels
private readonly IPipelinePersistenceService _persistenceService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private readonly IXpDataPathService _dataPathService;
private PipelineNodeViewModel _selectedNode;
private BitmapSource _sourceImage;
@@ -49,13 +51,15 @@ namespace XplorePlane.ViewModels
IPipelineExecutionService executionService,
IPipelinePersistenceService persistenceService,
IEventAggregator eventAggregator,
ILoggerService logger)
ILoggerService logger,
IXpDataPathService dataPathService)
{
_imageProcessingService = imageProcessingService ?? throw new ArgumentNullException(nameof(imageProcessingService));
_executionService = executionService ?? throw new ArgumentNullException(nameof(executionService));
_persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = logger?.ForModule<PipelineEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_dataPathService = dataPathService ?? throw new ArgumentNullException(nameof(dataPathService));
PipelineNodes = new ObservableCollection<PipelineNodeViewModel>();
AvailableDevices = new ObservableCollection<string>();
@@ -808,11 +812,9 @@ namespace XplorePlane.ViewModels
return $"已执行到“{ExecutionEndNode.DisplayName}” ({executionCount} 个有效节点)";
}
private static string GetPipelineDirectory()
private string GetPipelineDirectory()
{
var dir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"XplorePlane", "Pipelines");
var dir = _dataPathService.ToolsPath;
Directory.CreateDirectory(dir);
return dir;
}
+235 -50
View File
@@ -1,4 +1,4 @@
using Microsoft.Win32;
using Microsoft.Win32;
using Prism.Commands;
using Prism.Events;
using Prism.Ioc;
@@ -7,10 +7,13 @@ using System;
using System.Collections.ObjectModel;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using XplorePlane.Events;
using XplorePlane.Services.MainViewport;
using XplorePlane.Services.Storage;
using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views;
using XplorePlane.Views.Cnc;
@@ -29,6 +32,7 @@ namespace XplorePlane.ViewModels
private readonly IContainerProvider _containerProvider;
private readonly IEventAggregator _eventAggregator;
private readonly IMainViewportService _mainViewportService;
private readonly IXpDataPathService _xpDataPathService;
private readonly CncEditorViewModel _cncEditorViewModel;
private readonly CncPageView _cncPageView;
@@ -65,6 +69,10 @@ namespace XplorePlane.ViewModels
public DelegateCommand OpenLibraryVersionsCommand { get; }
public DelegateCommand OpenUserManualCommand { get; }
public DelegateCommand OpenCameraSettingsCommand { get; }
public DelegateCommand OpenSettingsCommand { get; }
public DelegateCommand BrowseDataRootPathCommand { get; }
public DelegateCommand ResetDataRootPathCommand { get; }
public DelegateCommand SaveDataRootPathCommand { get; }
public DelegateCommand NewCncProgramCommand { get; }
public DelegateCommand SaveCncProgramCommand { get; }
public DelegateCommand LoadCncProgramCommand { get; }
@@ -73,6 +81,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand InsertCompleteProgramCommand { get; }
public DelegateCommand InsertInspectionMarkerCommand { get; }
public DelegateCommand InsertInspectionModuleCommand { get; }
public DelegateCommand InsertBuiltInInspectionModuleCommand { get; }
public DelegateCommand InsertSaveNodeCommand { get; }
public DelegateCommand InsertPauseDialogCommand { get; }
public DelegateCommand InsertWaitDelayCommand { get; }
@@ -118,6 +127,34 @@ namespace XplorePlane.ViewModels
public bool IsUsingLiveDetectorSource => _mainViewportService.CurrentSourceMode == MainViewportSourceMode.LiveDetector;
public string DataRootPath
{
get => _dataRootPath;
set => SetProperty(ref _dataRootPath, value);
}
public string PlanRootPath => _xpDataPathService.PlanPath;
public string ToolsRootPath => _xpDataPathService.ToolsPath;
public string ResultsRootPath => _xpDataPathService.DataPath;
public string ReportRootPath => _xpDataPathService.ReportPath;
public ObservableCollection<BuiltInInspectionModuleItem> BuiltInInspectionModules { get; } = new();
public BuiltInInspectionModuleItem SelectedBuiltInInspectionModule
{
get => _selectedBuiltInInspectionModule;
set
{
if (SetProperty(ref _selectedBuiltInInspectionModule, value))
{
InsertBuiltInInspectionModuleCommand?.RaiseCanExecuteChanged();
}
}
}
/// <summary>右侧图像区域内容 | Right-side image panel content</summary>
public object ImagePanelContent
{
@@ -140,11 +177,11 @@ namespace XplorePlane.ViewModels
}
// 窗口引用(单例窗口防止重复打开)
private Window _motionDebugWindow;
private Window _detectorConfigWindow;
private Window _plcAddrConfigWindow;
private Window _realTimeLogViewerWindow;
private Window _settingsWindow;
private Window _toolboxWindow;
private Window _raySourceConfigWindow;
private object _imagePanelContent;
@@ -153,16 +190,21 @@ namespace XplorePlane.ViewModels
private bool _isCncEditorMode;
private string _licenseInfo = "当前时间";
private string _dataRootPath = string.Empty;
private BuiltInInspectionModuleItem _selectedBuiltInInspectionModule;
public MainViewModel(
ILoggerService logger,
IContainerProvider containerProvider,
IEventAggregator eventAggregator,
IMainViewportService mainViewportService)
IMainViewportService mainViewportService,
IXpDataPathService xpDataPathService)
{
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
_xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService));
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
@@ -183,8 +225,8 @@ namespace XplorePlane.ViewModels
var node = _cncEditorViewModel.SelectedNode;
if (node?.ResultImage != null)
{
_logger.Info("[图像链路] 切换到节点 [{Name}],显示缓存结果图像", node.Name);
_mainViewportService.SetManualImage(node.ResultImage, $"CNC节点:{node.Name}");
_logger.Info("[Image] Switched to node [{Name}], showing cached result image.", node.Name);
_mainViewportService.SetManualImage(node.ResultImage, $"CNC node: {node.Name}");
}
}
};
@@ -207,11 +249,15 @@ namespace XplorePlane.ViewModels
LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor);
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编"));
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编"));
OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox);
OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于"));
OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual);
OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings);
OpenSettingsCommand = new DelegateCommand(ExecuteOpenSettings);
BrowseDataRootPathCommand = new DelegateCommand(ExecuteBrowseDataRootPath);
ResetDataRootPathCommand = new DelegateCommand(ExecuteResetDataRootPath);
SaveDataRootPathCommand = new DelegateCommand(ExecuteSaveDataRootPath);
NewCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.NewProgramCommand.Execute()));
SaveCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.SaveProgramCommand.Execute()));
LoadCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.LoadProgramCommand.Execute()));
@@ -220,6 +266,9 @@ namespace XplorePlane.ViewModels
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertCompleteProgramCommand.Execute()));
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionMarkerCommand.Execute()));
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionModuleCommand.Execute()));
InsertBuiltInInspectionModuleCommand = new DelegateCommand(
async () => await ExecuteInsertBuiltInInspectionModuleAsync(),
CanExecuteInsertBuiltInInspectionModule);
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSaveNodeCommand.Execute()));
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute()));
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute()));
@@ -238,7 +287,6 @@ namespace XplorePlane.ViewModels
BubbleMeasureCommand = new DelegateCommand(ExecuteBubbleMeasure);
// 辅助线命令
ToggleCrosshairCommand = new DelegateCommand(() =>
_eventAggregator.GetEvent<ToggleCrosshairEvent>().Publish());
@@ -256,6 +304,8 @@ namespace XplorePlane.ViewModels
ImagePanelContent = new PipelineEditorView();
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
ImagePanelWidth = new GridLength(320);
DataRootPath = _xpDataPathService.RootPath;
LoadBuiltInInspectionModules();
_logger.Info("MainViewModel 已初始化");
}
@@ -289,7 +339,7 @@ namespace XplorePlane.ViewModels
private void ExecuteOpenToolbox()
{
ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱");
ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "Operator Toolbox");
}
private void ExecuteOpenCncEditor()
@@ -330,15 +380,15 @@ namespace XplorePlane.ViewModels
var manualPath = ConfigurationManager.AppSettings["UserManual"];
if (string.IsNullOrEmpty(manualPath))
{
_logger.Warn("未配置用户手册路径");
MessageBox.Show("未配置用户手册路径,请检查 App.config 中的 UserManual 配置项。",
_logger.Warn("User manual path is not configured.");
MessageBox.Show("User manual path is not configured. Please check the UserManual setting in App.config.",
"提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (!File.Exists(manualPath))
{
_logger.Warn("用户手册文件不存在:{Path}", manualPath);
_logger.Warn("User manual file not found: {Path}", manualPath);
MessageBox.Show($"用户手册文件不存在:\n{manualPath}",
"提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
@@ -351,8 +401,8 @@ namespace XplorePlane.ViewModels
}
catch (Exception ex)
{
_logger.Error(ex, "打开用户手册失败");
MessageBox.Show($"打开用户手册失败:{ex.Message}",
_logger.Error(ex, "Failed to open user manual.");
MessageBox.Show($"Failed to open user manual: {ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -373,13 +423,146 @@ namespace XplorePlane.ViewModels
}
catch (Exception ex)
{
_logger.Error(ex, "打开相机设置失败");
_logger.Error(ex, "Failed to open camera settings.");
}
}
private void ExecuteOpenSettings()
{
try
{
ShowOrActivate(_settingsWindow, w => _settingsWindow = w,
() => new Views.SettingsWindow(this), "Settings");
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to open settings window");
MessageBox.Show($"Failed to open settings window: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExecuteBrowseDataRootPath()
{
try
{
var dialog = new OpenFolderDialog
{
Title = "选择 XP 数据根目录",
InitialDirectory = Directory.Exists(DataRootPath) ? DataRootPath : _xpDataPathService.RootPath
};
if (dialog.ShowDialog() == true)
{
DataRootPath = dialog.FolderName;
}
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to browse XP data root.");
MessageBox.Show($"Failed to browse data root: {ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ExecuteResetDataRootPath()
{
DataRootPath = _xpDataPathService.DefaultRootPath;
}
private void ExecuteSaveDataRootPath()
{
try
{
_xpDataPathService.SaveRootPath(DataRootPath);
DataRootPath = _xpDataPathService.RootPath;
RaisePropertyChanged(nameof(PlanRootPath));
RaisePropertyChanged(nameof(ToolsRootPath));
RaisePropertyChanged(nameof(ResultsRootPath));
RaisePropertyChanged(nameof(ReportRootPath));
LoadBuiltInInspectionModules();
MessageBox.Show("XP data root saved. New save/load dialogs will use the new path immediately.",
"提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to save XP data root.");
MessageBox.Show($"Failed to save data root: {ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private bool CanExecuteInsertBuiltInInspectionModule()
{
return SelectedBuiltInInspectionModule != null;
}
private async Task ExecuteInsertBuiltInInspectionModuleAsync()
{
var module = SelectedBuiltInInspectionModule;
if (module == null)
return;
try
{
ShowCncEditor();
await _cncEditorViewModel.InsertInspectionModuleFromPipelineFileAsync(module.FilePath);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to insert built-in inspection module: {FilePath}", module.FilePath);
MessageBox.Show($"Failed to insert built-in inspection module: {ex.Message}",
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void LoadBuiltInInspectionModules()
{
BuiltInInspectionModules.Clear();
try
{
var toolsPath = _xpDataPathService.ToolsPath;
if (!Directory.Exists(toolsPath))
{
SelectedBuiltInInspectionModule = null;
return;
}
var files = Directory
.EnumerateFiles(toolsPath, "*.xpm", SearchOption.AllDirectories)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.Select(path => new BuiltInInspectionModuleItem(
GetBuiltInModuleDisplayName(toolsPath, path),
path))
.ToList();
foreach (var file in files)
{
BuiltInInspectionModules.Add(file);
}
SelectedBuiltInInspectionModule = BuiltInInspectionModules.FirstOrDefault();
_logger.Info("Loaded {Count} built-in inspection modules from {ToolsPath}", BuiltInInspectionModules.Count, toolsPath);
}
catch (Exception ex)
{
SelectedBuiltInInspectionModule = null;
_logger.Error(ex, "Failed to load built-in inspection modules.");
}
}
private static string GetBuiltInModuleDisplayName(string toolsPath, string filePath)
{
var relativePath = Path.GetRelativePath(toolsPath, filePath);
var withoutExtension = Path.ChangeExtension(relativePath, null) ?? relativePath;
return withoutExtension.Replace(Path.DirectorySeparatorChar, '/');
}
private void ExecuteAxisReset()
{
var result = MessageBox.Show("确认执行轴复位操作?", "轴复位",
var result = MessageBox.Show("Confirm axis reset?", "Axis Reset",
MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (result != MessageBoxResult.OK)
return;
@@ -396,7 +579,7 @@ namespace XplorePlane.ViewModels
}
catch (Exception ex)
{
_logger.Error(ex, "轴复位异常");
_logger.Error(ex, "Axis reset failed.");
MessageBox.Show($"轴复位异常:{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
@@ -407,12 +590,12 @@ namespace XplorePlane.ViewModels
try
{
ShowOrActivate(_detectorConfigWindow, w => _detectorConfigWindow = w,
() => new XP.Hardware.Detector.Views.DetectorConfigWindow(), "探测器配置");
() => new XP.Hardware.Detector.Views.DetectorConfigWindow(), "Detector Config");
}
catch (Exception ex)
{
_logger.Error(ex, "打开探测器配置窗口失败");
MessageBox.Show($"打开探测器配置窗口失败:\n{ex.InnerException?.Message ?? ex.Message}",
_logger.Error(ex, "Failed to open detector config window.");
MessageBox.Show($"Failed to open detector config window:\n{ex.InnerException?.Message ?? ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -432,7 +615,7 @@ namespace XplorePlane.ViewModels
private void ExecuteOpenRaySourceConfig()
{
ShowOrActivate(_raySourceConfigWindow, w => _raySourceConfigWindow = w,
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "射线源配置");
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "Ray Source Config");
}
private void ExecuteLoadImage()
@@ -455,20 +638,20 @@ namespace XplorePlane.ViewModels
bitmap.EndInit();
bitmap.Freeze();
_logger.Info("[图像链路] ExecuteLoadImage:加载图像 {Path},准备推送到 MainViewportService ManualImageLoadedEvent", dialog.FileName);
_logger.Info("[Image] ExecuteLoadImage loaded image {Path} and will push it to MainViewportService and ManualImageLoadedEvent.", dialog.FileName);
_mainViewportService.SetManualImage(bitmap, dialog.FileName);
// 同时发布事件,让 PipelineEditorViewModel 收到图像并触发流水线执行
// Publish the image to the pipeline editor at the same time.
_eventAggregator.GetEvent<ManualImageLoadedEvent>()
.Publish(new ManualImageLoadedPayload(bitmap, dialog.FileName));
_logger.Info("[图像链路] ManualImageLoadedEvent 已发布");
_logger.Info("[Image] ManualImageLoadedEvent published.");
RaisePropertyChanged(nameof(IsUsingLiveDetectorSource));
}
catch (Exception ex)
{
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
MessageBox.Show($"加载图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
_logger.Error(ex, "Failed to load image: {Path}", dialog.FileName);
MessageBox.Show($"Failed to load image: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -491,18 +674,18 @@ namespace XplorePlane.ViewModels
var result = raySourceService.WarmUp();
if (!result.Success)
{
MessageBox.Show($"暖机失败:{result.ErrorMessage}", "错误",
MessageBox.Show($"Warm-up failed: {result.ErrorMessage}", "Error",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
_logger.Info("暖机命令已发送");
_logger.Info("Warm-up command sent.");
}
}
catch (Exception ex)
{
_logger.Error(ex, "暖机异常");
MessageBox.Show($"暖机异常:{ex.Message}", "错误",
_logger.Error(ex, "Warm-up failed.");
MessageBox.Show($"Warm-up error: {ex.Message}", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -525,28 +708,28 @@ namespace XplorePlane.ViewModels
private void ExecutePointDistanceMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("点点距测量功能已触发");
_logger.Info("Point distance measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointDistance);
}
private void ExecutePointLineDistanceMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("点线距测量功能已触发");
_logger.Info("Point-line distance measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.PointLineDistance);
}
private void ExecuteAngleMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("角度测量功能已触发");
_logger.Info("Angle measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.Angle);
}
private void ExecuteThroughHoleFillRateMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("通孔填锡率测量功能已触发");
_logger.Info("Through-hole fill-rate measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.ThroughHoleFillRate);
}
@@ -555,7 +738,7 @@ namespace XplorePlane.ViewModels
private void ExecuteBgaVoidMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("BGA空隙测量功能已触发");
_logger.Info("BGA void measurement triggered.");
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BgaVoid);
if (_bgaMeasurePanel != null && _bgaMeasurePanel.IsVisible)
@@ -580,12 +763,12 @@ namespace XplorePlane.ViewModels
private void ExecuteBubbleMeasure()
{
if (!CheckImageLoaded()) return;
_logger.Info("气泡测量功能已触发");
_logger.Info("Bubble measurement triggered.");
// 进入气泡测量模式
// Enter bubble measurement mode.
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.BubbleMeasure);
// 弹出工具面板
// Open the tool panel.
if (_bubbleMeasurePanel != null && _bubbleMeasurePanel.IsVisible)
{
_bubbleMeasurePanel.Activate();
@@ -598,7 +781,7 @@ namespace XplorePlane.ViewModels
};
_bubbleMeasurePanel.Closed += (s, e) =>
{
// 关闭面板时退出气泡测量模式
// Exit bubble measurement mode when the panel closes.
_eventAggregator.GetEvent<MeasurementToolEvent>().Publish(MeasurementToolMode.None);
};
_bubbleMeasurePanel.Show();
@@ -619,7 +802,7 @@ namespace XplorePlane.ViewModels
}
catch (Exception ex)
{
_logger.Error(ex, "打开语言设置失败");
_logger.Error(ex, "Failed to open language settings.");
}
}
@@ -631,38 +814,38 @@ namespace XplorePlane.ViewModels
private void OnNavigateHome()
{
_logger.Info("导航到主页");
LicenseInfo = "页";
_logger.Info("Navigated to home.");
LicenseInfo = "页";
}
private void OnNavigateInspect()
{
_logger.Info("导航到检测页面");
LicenseInfo = "检测页面";
_logger.Info("Navigated to inspection page.");
LicenseInfo = "Inspection";
}
private void OnOpenFile()
{
_logger.Info("打开文件");
_logger.Info("Open file.");
LicenseInfo = "打开文件";
}
private void OnExport()
{
_logger.Info("导出数据");
_logger.Info("Export data.");
LicenseInfo = "导出数据";
}
private void OnClear()
{
_logger.Info("清除数据");
_logger.Info("Clear data.");
LicenseInfo = "清除数据";
}
private void OnEditProperties()
{
_logger.Info("编辑属性");
LicenseInfo = "编辑属性";
_logger.Info("Edit properties.");
LicenseInfo = "Edit properties";
}
private void OnMainViewportStateChanged(object sender, EventArgs e)
@@ -678,12 +861,14 @@ namespace XplorePlane.ViewModels
{
if (payload?.Image == null)
{
_logger.Warn("[图像链路] OnPipelinePreviewUpdatedpayload 或 Image null,跳过");
_logger.Warn("[Image] OnPipelinePreviewUpdated skipped because payload or image is null.");
return;
}
_logger.Info("[图像链路] OnPipelinePreviewUpdated:收到流水线结果图像,推送到 MainViewportService");
_logger.Info("[Image] OnPipelinePreviewUpdated received a pipeline preview image and pushed it to MainViewportService.");
_mainViewportService.SetManualImage(payload.Image, string.Empty);
}
public sealed record BuiltInInspectionModuleItem(string DisplayName, string FilePath);
#endregion
}
}
@@ -1,4 +1,4 @@
<UserControl
<UserControl
x:Class="XplorePlane.Views.PipelineEditorView"
x:Name="RootControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
@@ -38,15 +38,18 @@
<Style x:Key="ToolbarBtn" TargetType="Button">
<Setter Property="Height" Value="28" />
<Setter Property="MinWidth" Value="52" />
<Setter Property="Width" Value="32" />
<Setter Property="MinWidth" Value="32" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="Padding" Value="8,0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#CDCBCB" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</UserControl.Resources>
@@ -80,25 +83,40 @@
Orientation="Horizontal">
<Button
Command="{Binding NewPipelineCommand}"
Content="新建配方"
Style="{StaticResource ToolbarBtn}"
ToolTip="新建配方" />
ToolTip="新建配方">
<TextBlock
FontFamily="Segoe MDL2 Assets"
FontSize="14"
Text="&#xE710;" />
</Button>
<Button
Command="{Binding SavePipelineCommand}"
Content="保存配方"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存当前配方" />
ToolTip="保存当前配方">
<TextBlock
FontFamily="Segoe MDL2 Assets"
FontSize="14"
Text="&#xE74E;" />
</Button>
<Button
Width="64"
Command="{Binding SaveAsPipelineCommand}"
Content="另存为"
Style="{StaticResource ToolbarBtn}"
ToolTip="另存当前配方" />
ToolTip="另存当前配方">
<TextBlock
FontFamily="Segoe MDL2 Assets"
FontSize="14"
Text="&#xE792;" />
</Button>
<Button
Command="{Binding LoadPipelineCommand}"
Content="加载配方"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载配方" />
ToolTip="加载配方">
<TextBlock
FontFamily="Segoe MDL2 Assets"
FontSize="14"
Text="&#xE8E5;" />
</Button>
</StackPanel>
<TextBlock
@@ -433,3 +451,4 @@
</Grid>
</Border>
</UserControl>
+41 -7
View File
@@ -279,6 +279,33 @@
Size="Large"
SmallImage="/Assets/Icons/cnc.png"
Text="CNC 编辑" />
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
telerik:ScreenTip.Title="矩阵编排"
Command="{Binding OpenMatrixEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/matrix.png"
Text="矩阵编排" />
<StackPanel Width="170">
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Center"
Text="内置检测模块" />
<telerik:RadRibbonComboBox
Width="160"
ItemsSource="{Binding BuiltInInspectionModules}"
DisplayMemberPath="DisplayName"
SelectedItem="{Binding SelectedBuiltInInspectionModule}"
IsEditable="False" />
<telerik:RadRibbonButton
Margin="0,4,0,0"
telerik:ScreenTip.Description="从 Tools 目录扫描到的 .xpm 中选择一个配方,并插入到当前 CNC 程序中"
telerik:ScreenTip.Title="插入内置检测模块"
Command="{Binding InsertBuiltInInspectionModuleCommand}"
Size="Medium"
SmallImage="/Assets/Icons/Module.png"
Text="插入模块" />
</StackPanel>
<!--
<StackPanel>
<telerik:RadRibbonButton
@@ -331,13 +358,7 @@
</StackPanel>
-->
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
telerik:ScreenTip.Title="矩阵编排"
Command="{Binding OpenMatrixEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/matrix.png"
Text="矩阵编排" />
</telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
@@ -391,6 +412,19 @@
</telerik:RadRibbonTab>
<telerik:RadRibbonTab Header="设置">
<telerik:RadRibbonGroup Header="全局设置">
<telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" />
</telerik:RadRibbonGroup.Variants>
<telerik:RadRibbonButton
Size="Large"
SmallImage="/Assets/Icons/setting.png"
Command="{Binding OpenSettingsCommand}"
Text="全局设置" />
</telerik:RadRibbonGroup>
<telerik:RadRibbonGroup
telerik:ScreenTip.Description="Show the Alignment tab of the Format Cells dialog box."
telerik:ScreenTip.Title="Format Cells: Alignment"
+7 -1
View File
@@ -1,4 +1,8 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Linq;
using Telerik.Windows.Controls;
using XplorePlane.ViewModels;
namespace XplorePlane.Views
@@ -12,6 +16,8 @@ namespace XplorePlane.Views
{
InitializeComponent();
DataContext = viewModel;
}
}
}
}
@@ -0,0 +1,169 @@
<Window x:Class="XplorePlane.Views.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="系统设置"
Width="860"
Height="620"
MinWidth="760"
MinHeight="540"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"
Background="#F5F5F5">
<Window.Resources>
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="#1F1F1F" />
<Setter Property="Margin" Value="0,0,0,12" />
</Style>
<Style x:Key="CardStyle" TargetType="Border">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="#D9D9D9" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,0,12" />
</Style>
<Style x:Key="ActionButtonStyle" TargetType="Button">
<Setter Property="Height" Value="32" />
<Setter Property="MinWidth" Value="96" />
<Setter Property="Margin" Value="0,0,10,10" />
<Setter Property="Padding" Value="12,0" />
</Style>
</Window.Resources>
<DockPanel Margin="12">
<TabControl Background="White">
<TabItem Header="通用">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="通用设置" />
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock FontSize="13"
FontWeight="SemiBold"
Text="XP 数据目录" />
<TextBlock Margin="0,8,0,8"
Foreground="#666666"
Text="Plan 用于 CNC 默认保存和加载,Tools 用于流程图配方 xpm,Data 用于执行结果和中间图像,Report 为报告预留目录。" />
<Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="数据根目录" />
<TextBox Grid.Column="1"
Height="30"
Margin="0,0,10,0"
Padding="8,0"
Text="{Binding DataRootPath, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="2"
Command="{Binding BrowseDataRootPathCommand}"
Content="浏览"
Style="{StaticResource ActionButtonStyle}" />
<Button Grid.Column="3"
Command="{Binding ResetDataRootPathCommand}"
Content="恢复默认"
Style="{StaticResource ActionButtonStyle}" />
<Button Grid.Column="4"
Command="{Binding SaveDataRootPathCommand}"
Content="保存"
Style="{StaticResource ActionButtonStyle}" />
</Grid>
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock FontSize="13"
FontWeight="SemiBold"
Text="界面与使用习惯" />
<CheckBox Margin="0,12,0,0"
Content="启动时默认显示实时图像" />
<CheckBox Margin="0,8,0,0"
Content="允许自动恢复上次工作状态" />
<CheckBox Margin="0,8,0,0"
Content="启用状态栏详细提示" />
</StackPanel>
</Border>
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock FontSize="13"
FontWeight="SemiBold"
Text="调试" />
<WrapPanel>
<Button Command="{Binding OpenLibraryVersionsCommand}"
Content="版本信息"
Style="{StaticResource ActionButtonStyle}" />
</WrapPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="报告">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource SectionTitleStyle}" Text="报告设置" />
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<TextBlock FontSize="13"
FontWeight="SemiBold"
Text="报告模板" />
<TextBlock Margin="0,10,0,8"
Foreground="#666666"
Text="这里预留报告输出相关设置,可继续扩展公司信息、模板路径、签核信息和导出规则。" />
<Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="报告标题" />
<TextBox Grid.Column="1"
Height="30"
Padding="8,0"
Text="检测报告" />
</Grid>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="模板路径" />
<TextBox Grid.Column="1"
Height="30"
Padding="8,0"
Text="D:\XPData\Report\Templates\Default" />
</Grid>
<CheckBox Margin="0,12,0,0"
Content="导出时自动附带原始图像" />
<CheckBox Margin="0,8,0,0"
Content="导出时自动附带处理结果图像" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</DockPanel>
</Window>
@@ -0,0 +1,13 @@
using System.Windows;
namespace XplorePlane.Views
{
public partial class SettingsWindow : Window
{
public SettingsWindow(object viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
}