6 Commits

Author SHA1 Message Date
zhengxuan.zhang 8beed66ef0 #初步CNC编辑界面 2026-04-21 13:19:53 +08:00
zhengxuan.zhang b966e0b8e8 #CNC检测模块概念设计 2026-04-21 12:56:22 +08:00
zhengxuan.zhang 238e97d110 CNC数据存储问题,包括中间的处理情况的缓存 2026-04-21 07:32:28 +08:00
zhengxuan.zhang d9d3e31e57 树形节点图标直接复用项目现有图标资源;调整CNC编辑界面大小 2026-04-21 02:41:18 +08:00
zhengxuan.zhang 3b4b794dd0 #初步的cnc编辑功能,实现插入参考点的流程 2026-04-21 01:49:09 +08:00
zhengxuan.zhang c91b55785e CNC编辑模式与常规模式切换 2026-04-20 21:36:46 +08:00
25 changed files with 2773 additions and 541 deletions
@@ -1878,10 +1878,7 @@
"Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408" "Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
}, },
"runtime": { "runtime": {
"XP.Common.dll": { "XP.Common.dll": {}
"assemblyVersion": "1.4.16.1",
"fileVersion": "1.4.16.1"
}
}, },
"resources": { "resources": {
"en-US/XP.Common.resources.dll": { "en-US/XP.Common.resources.dll": {
@@ -0,0 +1,340 @@
using Moq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using XP.Common.Configs;
using XP.Common.Database.Implementations;
using XP.Common.Database.Interfaces;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.InspectionResults;
using Xunit;
namespace XplorePlane.Tests.Services
{
public class InspectionResultStoreTests : IDisposable
{
private readonly string _tempRoot;
private readonly Mock<ILoggerService> _mockLogger;
private readonly IDbContext _dbContext;
private readonly InspectionResultStore _store;
public InspectionResultStoreTests()
{
_tempRoot = Path.Combine(Path.GetTempPath(), "XplorePlane.Tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempRoot);
_mockLogger = new Mock<ILoggerService>();
_mockLogger.Setup(l => l.ForModule(It.IsAny<string>())).Returns(_mockLogger.Object);
_mockLogger.Setup(l => l.ForModule<InspectionResultStore>()).Returns(_mockLogger.Object);
var sqliteConfig = new SqliteConfig
{
DbFilePath = Path.Combine(_tempRoot, "inspection-results.db"),
CreateIfNotExists = true,
EnableWalMode = false,
EnableSqlLogging = false
};
_dbContext = new SqliteContext(sqliteConfig, _mockLogger.Object);
_store = new InspectionResultStore(_dbContext, _mockLogger.Object, Path.Combine(_tempRoot, "assets"));
}
[Fact]
public async Task FullRun_WithTwoNodes_CanRoundTripDetailAndQuery()
{
var startedAt = new DateTime(2026, 4, 21, 10, 0, 0, DateTimeKind.Utc);
var run = new InspectionRunRecord
{
ProgramName = "NewCncProgram",
WorkpieceId = "QFN_1",
SerialNumber = "SN-001",
StartedAt = startedAt
};
var runSource = CreateTempFile("run-source.bmp", "run-source");
await _store.BeginRunAsync(run, new InspectionAssetWriteRequest
{
AssetType = InspectionAssetType.RunSourceImage,
SourceFilePath = runSource,
FileFormat = "bmp"
});
var pipelineA = BuildPipeline("Recipe-A", ("GaussianBlur", 0), ("Threshold", 1));
var node1Id = Guid.NewGuid();
await _store.AppendNodeResultAsync(
new InspectionNodeResult
{
RunId = run.RunId,
NodeId = node1Id,
NodeIndex = 1,
NodeName = "检测节点1",
PipelineId = pipelineA.Id,
PipelineName = pipelineA.Name,
NodePass = true,
DurationMs = 135
},
new[]
{
new InspectionMetricResult
{
MetricKey = "bridge.rate",
MetricName = "Bridge Rate",
MetricValue = 0.12,
Unit = "%",
UpperLimit = 0.2,
IsPass = true,
DisplayOrder = 1
},
new InspectionMetricResult
{
MetricKey = "void.area",
MetricName = "Void Area",
MetricValue = 5.6,
Unit = "px",
UpperLimit = 8,
IsPass = true,
DisplayOrder = 2
}
},
new PipelineExecutionSnapshot
{
PipelineName = pipelineA.Name,
PipelineDefinitionJson = JsonSerializer.Serialize(pipelineA)
},
new[]
{
new InspectionAssetWriteRequest
{
AssetType = InspectionAssetType.NodeInputImage,
SourceFilePath = CreateTempFile("node1-input.bmp", "node1-input"),
FileFormat = "bmp"
},
new InspectionAssetWriteRequest
{
AssetType = InspectionAssetType.NodeResultImage,
SourceFilePath = CreateTempFile("node1-result.bmp", "node1-result"),
FileFormat = "bmp"
}
});
var pipelineB = BuildPipeline("Recipe-B", ("MeanFilter", 0), ("ContourDetection", 1));
var node2Id = Guid.NewGuid();
await _store.AppendNodeResultAsync(
new InspectionNodeResult
{
RunId = run.RunId,
NodeId = node2Id,
NodeIndex = 2,
NodeName = "检测节点2",
PipelineId = pipelineB.Id,
PipelineName = pipelineB.Name,
NodePass = false,
Status = InspectionNodeStatus.Failed,
DurationMs = 240
},
new[]
{
new InspectionMetricResult
{
MetricKey = "solder.height",
MetricName = "Solder Height",
MetricValue = 1.7,
Unit = "mm",
LowerLimit = 1.8,
IsPass = false,
DisplayOrder = 1
}
},
new PipelineExecutionSnapshot
{
PipelineName = pipelineB.Name,
PipelineDefinitionJson = JsonSerializer.Serialize(pipelineB)
},
new[]
{
new InspectionAssetWriteRequest
{
AssetType = InspectionAssetType.NodeResultImage,
SourceFilePath = CreateTempFile("node2-result.bmp", "node2-result"),
FileFormat = "bmp"
}
});
await _store.CompleteRunAsync(run.RunId);
var queried = await _store.QueryRunsAsync(new InspectionRunQuery
{
ProgramName = "NewCncProgram",
WorkpieceId = "QFN_1",
PipelineName = "Recipe-A"
});
var detail = await _store.GetRunDetailAsync(run.RunId);
Assert.Single(queried);
Assert.Equal(run.RunId, queried[0].RunId);
Assert.False(detail.Run.OverallPass);
Assert.Equal(2, detail.Run.NodeCount);
Assert.Equal(2, detail.Nodes.Count);
Assert.Equal(3, detail.Metrics.Count);
Assert.Equal(4, detail.Assets.Count);
Assert.Equal(2, detail.PipelineSnapshots.Count);
Assert.Contains(detail.Nodes, n => n.NodeId == node1Id && n.NodePass);
Assert.Contains(detail.Nodes, n => n.NodeId == node2Id && !n.NodePass);
Assert.All(detail.PipelineSnapshots, snapshot => Assert.False(string.IsNullOrWhiteSpace(snapshot.PipelineHash)));
var manifestPath = Path.Combine(_tempRoot, "assets", detail.Run.ResultRootPath.Replace('/', Path.DirectorySeparatorChar), "manifest.json");
Assert.True(File.Exists(manifestPath));
}
[Fact]
public async Task AppendNodeResult_MissingAsset_DoesNotCrashAndMarksAssetMissing()
{
var run = new InspectionRunRecord
{
ProgramName = "Program-A",
WorkpieceId = "Part-01",
SerialNumber = "SN-404"
};
await _store.BeginRunAsync(run);
var nodeId = Guid.NewGuid();
await _store.AppendNodeResultAsync(
new InspectionNodeResult
{
RunId = run.RunId,
NodeId = nodeId,
NodeIndex = 1,
NodeName = "缺图节点",
PipelineId = Guid.NewGuid(),
PipelineName = "Recipe-Missing",
NodePass = true
},
new[]
{
new InspectionMetricResult
{
MetricKey = "metric.only",
MetricName = "Metric Only",
MetricValue = 1,
Unit = "pcs",
IsPass = true,
DisplayOrder = 1
}
},
new PipelineExecutionSnapshot
{
PipelineName = "Recipe-Missing",
PipelineDefinitionJson = "{\"nodes\":[\"gaussian\"]}"
},
new[]
{
new InspectionAssetWriteRequest
{
AssetType = InspectionAssetType.NodeResultImage,
SourceFilePath = Path.Combine(_tempRoot, "missing-file.bmp"),
FileFormat = "bmp"
}
});
var detail = await _store.GetRunDetailAsync(run.RunId);
var node = Assert.Single(detail.Nodes);
Assert.Equal(InspectionNodeStatus.AssetMissing, node.Status);
Assert.Single(detail.Metrics);
Assert.Empty(detail.Assets);
}
[Fact]
public async Task PipelineSnapshot_IsStoredAsExecutionSnapshot_NotDependentOnLaterChanges()
{
var run = new InspectionRunRecord
{
ProgramName = "Program-Snapshot",
WorkpieceId = "Part-02",
SerialNumber = "SN-SNAP"
};
await _store.BeginRunAsync(run);
var pipeline = BuildPipeline("Recipe-Snapshot", ("GaussianBlur", 0), ("ContourDetection", 1));
var snapshotJson = JsonSerializer.Serialize(pipeline);
var originalHash = ComputeExpectedHash(snapshotJson);
await _store.AppendNodeResultAsync(
new InspectionNodeResult
{
RunId = run.RunId,
NodeId = Guid.NewGuid(),
NodeIndex = 1,
NodeName = "快照节点",
PipelineId = pipeline.Id,
PipelineName = pipeline.Name,
NodePass = true
},
pipelineSnapshot: new PipelineExecutionSnapshot
{
PipelineName = pipeline.Name,
PipelineDefinitionJson = snapshotJson
});
pipeline.Name = "Recipe-Snapshot-Changed";
pipeline.Nodes[0].OperatorKey = "MeanFilter";
var detail = await _store.GetRunDetailAsync(run.RunId);
var snapshot = Assert.Single(detail.PipelineSnapshots);
Assert.Equal("Recipe-Snapshot", snapshot.PipelineName);
Assert.Equal(snapshotJson, snapshot.PipelineDefinitionJson);
Assert.Equal(originalHash, snapshot.PipelineHash);
}
public void Dispose()
{
_dbContext.Dispose();
if (Directory.Exists(_tempRoot))
{
try
{
Directory.Delete(_tempRoot, true);
}
catch (IOException)
{
// SQLite file handles may release slightly after test teardown.
}
}
}
private string CreateTempFile(string fileName, string content)
{
var path = Path.Combine(_tempRoot, fileName);
File.WriteAllText(path, content);
return path;
}
private static PipelineModel BuildPipeline(string name, params (string OperatorKey, int Order)[] nodes)
{
return new PipelineModel
{
Name = name,
Nodes = nodes.Select(node => new PipelineNodeModel
{
OperatorKey = node.OperatorKey,
Order = node.Order
}).ToList()
};
}
private static string ComputeExpectedHash(string value)
{
using var sha = System.Security.Cryptography.SHA256.Create();
var bytes = System.Text.Encoding.UTF8.GetBytes(value);
return Convert.ToHexString(sha.ComputeHash(bytes));
}
}
}
@@ -34,11 +34,4 @@
<ProjectReference Include="..\XplorePlane\XplorePlane.csproj" /> <ProjectReference Include="..\XplorePlane\XplorePlane.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="ImageProcessing.Core">
<HintPath>..\XplorePlane\Libs\ImageProcessing\ImageProcessing.Core.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
</Project> </Project>
+3 -1
View File
@@ -36,6 +36,7 @@ using XplorePlane.Services.Camera;
using XplorePlane.Services.Cnc; using XplorePlane.Services.Cnc;
using XplorePlane.Services.Matrix; using XplorePlane.Services.Matrix;
using XplorePlane.Services.Measurement; using XplorePlane.Services.Measurement;
using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.Recipe; using XplorePlane.Services.Recipe;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.Cnc;
@@ -317,6 +318,7 @@ namespace XplorePlane
containerRegistry.RegisterSingleton<ICncProgramService, CncProgramService>(); containerRegistry.RegisterSingleton<ICncProgramService, CncProgramService>();
containerRegistry.RegisterSingleton<IMatrixService, MatrixService>(); containerRegistry.RegisterSingleton<IMatrixService, MatrixService>();
containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>(); containerRegistry.RegisterSingleton<IMeasurementDataService, MeasurementDataService>();
containerRegistry.RegisterSingleton<IInspectionResultStore, InspectionResultStore>();
// ── CNC / 矩阵 ViewModel(瞬态)── // ── CNC / 矩阵 ViewModel(瞬态)──
containerRegistry.Register<CncEditorViewModel>(); containerRegistry.Register<CncEditorViewModel>();
@@ -354,4 +356,4 @@ namespace XplorePlane
base.ConfigureModuleCatalog(moduleCatalog); base.ConfigureModuleCatalog(moduleCatalog);
} }
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

+3 -2
View File
@@ -46,7 +46,8 @@ namespace XplorePlane.Models
/// <summary>参考点节点 | Reference point node</summary> /// <summary>参考点节点 | Reference point node</summary>
public record ReferencePointNode( public record ReferencePointNode(
Guid Id, int Index, string Name, Guid Id, int Index, string Name,
double XM, double YM, double ZT, double ZD, double TiltD, double Dist double XM, double YM, double ZT, double ZD, double TiltD, double Dist,
bool IsRayOn, double Voltage, double Current
) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name); ) : CncNode(Id, Index, CncNodeType.ReferencePoint, Name);
/// <summary>保存节点(含图像)| Save node with image</summary> /// <summary>保存节点(含图像)| Save node with image</summary>
@@ -113,4 +114,4 @@ namespace XplorePlane.Models
DateTime UpdatedAt, DateTime UpdatedAt,
IReadOnlyList<CncNode> Nodes IReadOnlyList<CncNode> Nodes
); );
} }
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
namespace XplorePlane.Models
{
public enum InspectionAssetType
{
RunSourceImage,
NodeInputImage,
NodeResultImage
}
public enum InspectionNodeStatus
{
Succeeded,
Failed,
PartialSuccess,
AssetMissing
}
public class InspectionRunRecord
{
public Guid RunId { get; set; } = Guid.NewGuid();
public string ProgramName { get; set; } = string.Empty;
public string WorkpieceId { get; set; } = string.Empty;
public string SerialNumber { get; set; } = string.Empty;
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
public DateTime? CompletedAt { get; set; }
public bool OverallPass { get; set; }
public string SourceImagePath { get; set; } = string.Empty;
public string ResultRootPath { get; set; } = string.Empty;
public int NodeCount { get; set; }
}
public class InspectionNodeResult
{
public Guid RunId { get; set; }
public Guid NodeId { get; set; } = Guid.NewGuid();
public int NodeIndex { get; set; }
public string NodeName { get; set; } = string.Empty;
public Guid PipelineId { get; set; }
public string PipelineName { get; set; } = string.Empty;
public string PipelineVersionHash { get; set; } = string.Empty;
public bool NodePass { get; set; }
public string SourceImagePath { get; set; } = string.Empty;
public string ResultImagePath { get; set; } = string.Empty;
public InspectionNodeStatus Status { get; set; } = InspectionNodeStatus.Succeeded;
public long DurationMs { get; set; }
}
public class InspectionMetricResult
{
public Guid RunId { get; set; }
public Guid NodeId { get; set; }
public string MetricKey { get; set; } = string.Empty;
public string MetricName { get; set; } = string.Empty;
public double MetricValue { get; set; }
public string Unit { get; set; } = string.Empty;
public double? LowerLimit { get; set; }
public double? UpperLimit { get; set; }
public bool IsPass { get; set; }
public int DisplayOrder { get; set; }
}
public class InspectionAssetRecord
{
public Guid RunId { get; set; }
public Guid? NodeId { get; set; }
public InspectionAssetType AssetType { get; set; }
public string RelativePath { get; set; } = string.Empty;
public string FileFormat { get; set; } = string.Empty;
public int Width { get; set; }
public int Height { get; set; }
}
public class PipelineExecutionSnapshot
{
public Guid RunId { get; set; }
public Guid NodeId { get; set; }
public string PipelineName { get; set; } = string.Empty;
public string PipelineDefinitionJson { get; set; } = string.Empty;
public string PipelineHash { get; set; } = string.Empty;
}
public class InspectionAssetWriteRequest
{
public InspectionAssetType AssetType { get; set; }
public string FileName { get; set; } = string.Empty;
public string SourceFilePath { get; set; } = string.Empty;
public byte[] Content { get; set; }
public string FileFormat { get; set; } = string.Empty;
public int Width { get; set; }
public int Height { get; set; }
}
public class InspectionRunQuery
{
public string ProgramName { get; set; } = string.Empty;
public string WorkpieceId { get; set; } = string.Empty;
public string SerialNumber { get; set; } = string.Empty;
public string PipelineName { get; set; } = string.Empty;
public DateTime? From { get; set; }
public DateTime? To { get; set; }
public int? Skip { get; set; }
public int? Take { get; set; }
}
public class InspectionRunDetail
{
public InspectionRunRecord Run { get; set; } = new();
public IReadOnlyList<InspectionNodeResult> Nodes { get; set; } = Array.Empty<InspectionNodeResult>();
public IReadOnlyList<InspectionMetricResult> Metrics { get; set; } = Array.Empty<InspectionMetricResult>();
public IReadOnlyList<InspectionAssetRecord> Assets { get; set; } = Array.Empty<InspectionAssetRecord>();
public IReadOnlyList<PipelineExecutionSnapshot> PipelineSnapshots { get; set; } = Array.Empty<PipelineExecutionSnapshot>();
}
}
+56 -2
View File
@@ -6,6 +6,7 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Services;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.AppState; using XplorePlane.Services.AppState;
@@ -20,6 +21,7 @@ namespace XplorePlane.Services.Cnc
public class CncProgramService : ICncProgramService public class CncProgramService : ICncProgramService
{ {
private readonly IAppStateService _appStateService; private readonly IAppStateService _appStateService;
private readonly IRaySourceService _raySourceService;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
// ── 序列化配置 | Serialization options ── // ── 序列化配置 | Serialization options ──
@@ -32,12 +34,15 @@ namespace XplorePlane.Services.Cnc
public CncProgramService( public CncProgramService(
IAppStateService appStateService, IAppStateService appStateService,
IRaySourceService raySourceService,
ILoggerService logger) ILoggerService logger)
{ {
ArgumentNullException.ThrowIfNull(appStateService); ArgumentNullException.ThrowIfNull(appStateService);
ArgumentNullException.ThrowIfNull(raySourceService);
ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(logger);
_appStateService = appStateService; _appStateService = appStateService;
_raySourceService = raySourceService;
_logger = logger.ForModule<CncProgramService>(); _logger = logger.ForModule<CncProgramService>();
_logger.Info("CncProgramService 已初始化 | CncProgramService initialized"); _logger.Info("CncProgramService 已初始化 | CncProgramService initialized");
@@ -200,6 +205,32 @@ namespace XplorePlane.Services.Cnc
return updated; return updated;
} }
/// <inheritdoc />
public CncProgram UpdateNode(CncProgram program, int index, CncNode node)
{
ArgumentNullException.ThrowIfNull(program);
ArgumentNullException.ThrowIfNull(node);
if (index < 0 || index >= program.Nodes.Count)
throw new ArgumentOutOfRangeException(nameof(index),
$"Index out of range: {index}, Count={program.Nodes.Count}");
var nodes = new List<CncNode>(program.Nodes)
{
[index] = node with { Index = index }
};
var updated = program with
{
Nodes = nodes.AsReadOnly(),
UpdatedAt = DateTime.UtcNow
};
_logger.Info("Updated node: Index={Index}, Type={NodeType}, Program={ProgramName}",
index, node.NodeType, program.Name);
return updated;
}
/// <inheritdoc /> /// <inheritdoc />
public async Task SaveAsync(CncProgram program, string filePath) public async Task SaveAsync(CncProgram program, string filePath)
{ {
@@ -344,6 +375,7 @@ namespace XplorePlane.Services.Cnc
private ReferencePointNode CreateReferencePointNode(Guid id, int index) private ReferencePointNode CreateReferencePointNode(Guid id, int index)
{ {
var motion = _appStateService.MotionState; var motion = _appStateService.MotionState;
var raySource = _appStateService.RaySourceState;
return new ReferencePointNode( return new ReferencePointNode(
id, index, $"参考点_{index}", id, index, $"参考点_{index}",
XM: motion.XM, XM: motion.XM,
@@ -351,7 +383,10 @@ namespace XplorePlane.Services.Cnc
ZT: motion.ZT, ZT: motion.ZT,
ZD: motion.ZD, ZD: motion.ZD,
TiltD: motion.TiltD, TiltD: motion.TiltD,
Dist: motion.Dist); Dist: motion.Dist,
IsRayOn: raySource.IsOn,
Voltage: raySource.Voltage,
Current: TryReadCurrent());
} }
/// <summary>创建保存节点(含图像)| Create save node with image</summary> /// <summary>创建保存节点(含图像)| Create save node with image</summary>
@@ -382,5 +417,24 @@ namespace XplorePlane.Services.Cnc
id, index, $"保存位置_{index}", id, index, $"保存位置_{index}",
MotionState: _appStateService.MotionState); MotionState: _appStateService.MotionState);
} }
private double TryReadCurrent()
{
try
{
var result = _raySourceService.ReadCurrent();
if (result?.Success == true)
{
return result.GetFloat();
}
_logger.Warn("Failed to read ray source current, ReferencePoint node will use 0");
}
catch (Exception ex)
{
_logger.Warn("Failed to read ray source current: {Message}", ex.Message);
}
return 0d;
}
} }
} }
+4 -12
View File
@@ -4,36 +4,28 @@ using XplorePlane.Models;
namespace XplorePlane.Services.Cnc namespace XplorePlane.Services.Cnc
{ {
/// <summary> /// <summary>
/// CNC 程序管理服务接口,负责程序的创建、节点编辑、序列化/反序列化和文件读写 /// CNC program management service interface.
/// CNC program management service interface for creation, node editing, serialization and file I/O
/// </summary> /// </summary>
public interface ICncProgramService public interface ICncProgramService
{ {
/// <summary>创建空的 CNC 程序 | Create an empty CNC program</summary>
CncProgram CreateProgram(string name); CncProgram CreateProgram(string name);
/// <summary>根据节点类型创建节点(从 IAppStateService 捕获设备状态)| Create a node by type (captures device state from IAppStateService)</summary>
CncNode CreateNode(CncNodeType type); CncNode CreateNode(CncNodeType type);
/// <summary>在指定索引之后插入节点并重新编号 | Insert a node after the given index and renumber</summary>
CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node); CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node);
/// <summary>移除指定索引的节点并重新编号 | Remove the node at the given index and renumber</summary>
CncProgram RemoveNode(CncProgram program, int index); CncProgram RemoveNode(CncProgram program, int index);
/// <summary>将节点从旧索引移动到新索引并重新编号 | Move a node from old index to new index and renumber</summary>
CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex); CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex);
/// <summary>将 CNC 程序保存到 .xp 文件 | Save CNC program to .xp file</summary> CncProgram UpdateNode(CncProgram program, int index, CncNode node);
Task SaveAsync(CncProgram program, string filePath); Task SaveAsync(CncProgram program, string filePath);
/// <summary>从 .xp 文件加载 CNC 程序 | Load CNC program from .xp file</summary>
Task<CncProgram> LoadAsync(string filePath); Task<CncProgram> LoadAsync(string filePath);
/// <summary>将 CNC 程序序列化为 JSON 字符串 | Serialize CNC program to JSON string</summary>
string Serialize(CncProgram program); string Serialize(CncProgram program);
/// <summary>从 JSON 字符串反序列化 CNC 程序 | Deserialize CNC program from JSON string</summary>
CncProgram Deserialize(string json); CncProgram Deserialize(string json);
} }
} }
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using XplorePlane.Models;
namespace XplorePlane.Services.InspectionResults
{
public interface IInspectionResultStore
{
Task BeginRunAsync(InspectionRunRecord runRecord, InspectionAssetWriteRequest runSourceAsset = null);
Task AppendNodeResultAsync(
InspectionNodeResult nodeResult,
IEnumerable<InspectionMetricResult> metrics = null,
PipelineExecutionSnapshot pipelineSnapshot = null,
IEnumerable<InspectionAssetWriteRequest> assets = null);
Task CompleteRunAsync(Guid runId, bool? overallPass = null, DateTime? completedAt = null);
Task<IReadOnlyList<InspectionRunRecord>> QueryRunsAsync(InspectionRunQuery query = null);
Task<InspectionRunDetail> GetRunDetailAsync(Guid runId);
}
}
File diff suppressed because it is too large Load Diff
+99 -158
View File
@@ -3,6 +3,7 @@ using Prism.Commands;
using Prism.Events; using Prism.Events;
using Prism.Mvvm; using Prism.Mvvm;
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@@ -18,27 +19,21 @@ using XplorePlane.Services.Cnc;
namespace XplorePlane.ViewModels.Cnc namespace XplorePlane.ViewModels.Cnc
{ {
/// <summary> /// <summary>
/// CNC 编辑器 ViewModel,管理 CNC 程序的节点列表、编辑操作和文件操作 /// CNC editor ViewModel that manages the node tree, editing operations and file operations.
/// CNC editor ViewModel that manages the node list, editing operations and file operations
/// </summary> /// </summary>
public class CncEditorViewModel : BindableBase public class CncEditorViewModel : BindableBase
{ {
private readonly ICncProgramService _cncProgramService; private readonly ICncProgramService _cncProgramService;
private readonly IAppStateService _appStateService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
// 当前 CNC 程序 | Current CNC program
private CncProgram _currentProgram; private CncProgram _currentProgram;
private ObservableCollection<CncNodeViewModel> _nodes; private ObservableCollection<CncNodeViewModel> _nodes;
private ObservableCollection<CncNodeViewModel> _treeNodes;
private CncNodeViewModel _selectedNode; private CncNodeViewModel _selectedNode;
private bool _isModified; private bool _isModified;
private string _programName; private string _programName;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public CncEditorViewModel( public CncEditorViewModel(
ICncProgramService cncProgramService, ICncProgramService cncProgramService,
IAppStateService appStateService, IAppStateService appStateService,
@@ -46,13 +41,13 @@ namespace XplorePlane.ViewModels.Cnc
ILoggerService logger) ILoggerService logger)
{ {
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService)); _cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService)); ArgumentNullException.ThrowIfNull(appStateService);
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>(); _logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<CncEditorViewModel>();
_nodes = new ObservableCollection<CncNodeViewModel>(); _nodes = new ObservableCollection<CncNodeViewModel>();
_treeNodes = new ObservableCollection<CncNodeViewModel>();
// ── 节点插入命令 | Node insertion commands ──
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint)); InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage)); InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode)); InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode));
@@ -63,117 +58,79 @@ namespace XplorePlane.ViewModels.Cnc
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay)); InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay));
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram)); InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram));
// ── 节点编辑命令 | Node editing commands ──
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode) DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
.ObservesProperty(() => SelectedNode); .ObservesProperty(() => SelectedNode);
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp); MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown); MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
// ── 文件操作命令 | File operation commands ──
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync()); SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync()); LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
NewProgramCommand = new DelegateCommand(ExecuteNewProgram); NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv); ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
_logger.Info("CncEditorViewModel 已初始化 | CncEditorViewModel initialized"); _logger.Info("CncEditorViewModel initialized");
} }
// ── 属性 | Properties ──────────────────────────────────────────
/// <summary>节点列表 | Node list</summary>
public ObservableCollection<CncNodeViewModel> Nodes public ObservableCollection<CncNodeViewModel> Nodes
{ {
get => _nodes; get => _nodes;
set => SetProperty(ref _nodes, value); private set => SetProperty(ref _nodes, value);
}
public ObservableCollection<CncNodeViewModel> TreeNodes
{
get => _treeNodes;
private set => SetProperty(ref _treeNodes, value);
} }
/// <summary>当前选中的节点 | Currently selected node</summary>
public CncNodeViewModel SelectedNode public CncNodeViewModel SelectedNode
{ {
get => _selectedNode; get => _selectedNode;
set => SetProperty(ref _selectedNode, value); set
{
if (SetProperty(ref _selectedNode, value))
{
RaisePropertyChanged(nameof(HasSelection));
}
}
} }
/// <summary>程序是否已修改 | Whether the program has been modified</summary> public bool HasSelection => SelectedNode != null;
public bool IsModified public bool IsModified
{ {
get => _isModified; get => _isModified;
set => SetProperty(ref _isModified, value); set => SetProperty(ref _isModified, value);
} }
/// <summary>当前程序名称 | Current program name</summary>
public string ProgramName public string ProgramName
{ {
get => _programName; get => _programName;
set => SetProperty(ref _programName, value); set => SetProperty(ref _programName, value);
} }
// ── 节点插入命令 | Node insertion commands ──────────────────────
/// <summary>插入参考点命令 | Insert reference point command</summary>
public DelegateCommand InsertReferencePointCommand { get; } public DelegateCommand InsertReferencePointCommand { get; }
/// <summary>插入保存节点(含图像)命令 | Insert save node with image command</summary>
public DelegateCommand InsertSaveNodeWithImageCommand { get; } public DelegateCommand InsertSaveNodeWithImageCommand { get; }
/// <summary>插入保存节点命令 | Insert save node command</summary>
public DelegateCommand InsertSaveNodeCommand { get; } public DelegateCommand InsertSaveNodeCommand { get; }
/// <summary>插入保存位置命令 | Insert save position command</summary>
public DelegateCommand InsertSavePositionCommand { get; } public DelegateCommand InsertSavePositionCommand { get; }
/// <summary>插入检测模块命令 | Insert inspection module command</summary>
public DelegateCommand InsertInspectionModuleCommand { get; } public DelegateCommand InsertInspectionModuleCommand { get; }
/// <summary>插入检测标记命令 | Insert inspection marker command</summary>
public DelegateCommand InsertInspectionMarkerCommand { get; } public DelegateCommand InsertInspectionMarkerCommand { get; }
/// <summary>插入停顿对话框命令 | Insert pause dialog command</summary>
public DelegateCommand InsertPauseDialogCommand { get; } public DelegateCommand InsertPauseDialogCommand { get; }
/// <summary>插入等待延时命令 | Insert wait delay command</summary>
public DelegateCommand InsertWaitDelayCommand { get; } public DelegateCommand InsertWaitDelayCommand { get; }
/// <summary>插入完成程序命令 | Insert complete program command</summary>
public DelegateCommand InsertCompleteProgramCommand { get; } public DelegateCommand InsertCompleteProgramCommand { get; }
// ── 节点编辑命令 | Node editing commands ────────────────────────
/// <summary>删除选中节点命令 | Delete selected node command</summary>
public DelegateCommand DeleteNodeCommand { get; } public DelegateCommand DeleteNodeCommand { get; }
/// <summary>上移节点命令 | Move node up command</summary>
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; } public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
/// <summary>下移节点命令 | Move node down command</summary>
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; } public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
// ── 文件操作命令 | File operation commands ──────────────────────
/// <summary>保存程序命令 | Save program command</summary>
public DelegateCommand SaveProgramCommand { get; } public DelegateCommand SaveProgramCommand { get; }
/// <summary>加载程序命令 | Load program command</summary>
public DelegateCommand LoadProgramCommand { get; } public DelegateCommand LoadProgramCommand { get; }
/// <summary>新建程序命令 | New program command</summary>
public DelegateCommand NewProgramCommand { get; } public DelegateCommand NewProgramCommand { get; }
/// <summary>导出 CSV 命令 | Export CSV command</summary>
public DelegateCommand ExportCsvCommand { get; } public DelegateCommand ExportCsvCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
/// 插入指定类型的节点到选中节点之后
/// Insert a node of the specified type after the selected node
/// </summary>
private void ExecuteInsertNode(CncNodeType nodeType) private void ExecuteInsertNode(CncNodeType nodeType)
{ {
if (_currentProgram == null) if (_currentProgram == null)
{ {
_logger.Warn("无法插入节点:当前无程序 | Cannot insert node: no current program"); ExecuteNewProgram();
return;
} }
try try
@@ -183,18 +140,14 @@ namespace XplorePlane.ViewModels.Cnc
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node); _currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
OnProgramEdited(); OnProgramEdited();
_logger.Info("已插入节点 | Inserted node: Type={NodeType}", nodeType); _logger.Info("Inserted node: Type={NodeType}", nodeType);
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
// 重复插入 CompleteProgram 等业务规则异常 | Business rule exceptions like duplicate CompleteProgram _logger.Warn("Node insertion blocked: {Message}", ex.Message);
_logger.Warn("插入节点被阻止 | Node insertion blocked: {Message}", ex.Message);
} }
} }
/// <summary>
/// 删除选中节点 | Delete the selected node
/// </summary>
private void ExecuteDeleteNode() private void ExecuteDeleteNode()
{ {
if (_currentProgram == null || SelectedNode == null) if (_currentProgram == null || SelectedNode == null)
@@ -204,18 +157,14 @@ namespace XplorePlane.ViewModels.Cnc
{ {
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index); _currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
OnProgramEdited(); OnProgramEdited();
_logger.Info("已删除节点 | Deleted node at index: {Index}", SelectedNode.Index); _logger.Info("Deleted node at index: {Index}", SelectedNode.Index);
} }
catch (ArgumentOutOfRangeException ex) catch (ArgumentOutOfRangeException ex)
{ {
_logger.Warn("删除节点失败 | Delete node failed: {Message}", ex.Message); _logger.Warn("Delete node failed: {Message}", ex.Message);
} }
} }
/// <summary>
/// 判断是否可以删除节点(至少保留 1 个节点)
/// Determines whether delete is allowed (at least 1 node must remain)
/// </summary>
private bool CanExecuteDeleteNode() private bool CanExecuteDeleteNode()
{ {
return SelectedNode != null return SelectedNode != null
@@ -223,9 +172,6 @@ namespace XplorePlane.ViewModels.Cnc
&& _currentProgram.Nodes.Count > 1; && _currentProgram.Nodes.Count > 1;
} }
/// <summary>
/// 上移节点 | Move node up
/// </summary>
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm) private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
{ {
if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0) if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
@@ -235,17 +181,13 @@ namespace XplorePlane.ViewModels.Cnc
{ {
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1); _currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1);
OnProgramEdited(); OnProgramEdited();
_logger.Info("已上移节点 | Moved node up: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index - 1);
} }
catch (ArgumentOutOfRangeException ex) catch (ArgumentOutOfRangeException ex)
{ {
_logger.Warn("上移节点失败 | Move node up failed: {Message}", ex.Message); _logger.Warn("Move node up failed: {Message}", ex.Message);
} }
} }
/// <summary>
/// 下移节点 | Move node down
/// </summary>
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm) private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
{ {
if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1) if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
@@ -255,22 +197,18 @@ namespace XplorePlane.ViewModels.Cnc
{ {
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1); _currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1);
OnProgramEdited(); OnProgramEdited();
_logger.Info("已下移节点 | Moved node down: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index + 1);
} }
catch (ArgumentOutOfRangeException ex) catch (ArgumentOutOfRangeException ex)
{ {
_logger.Warn("下移节点失败 | Move node down failed: {Message}", ex.Message); _logger.Warn("Move node down failed: {Message}", ex.Message);
} }
} }
/// <summary>
/// 保存当前程序到文件 | Save current program to file
/// </summary>
private async Task ExecuteSaveProgramAsync() private async Task ExecuteSaveProgramAsync()
{ {
if (_currentProgram == null) if (_currentProgram == null)
{ {
_logger.Warn("无法保存:当前无程序 | Cannot save: no current program"); _logger.Warn("Cannot save: no current program");
return; return;
} }
@@ -289,17 +227,13 @@ namespace XplorePlane.ViewModels.Cnc
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName); await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
IsModified = false; IsModified = false;
_logger.Info("程序已保存 | Program saved: {FilePath}", dlg.FileName);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "保存程序失败 | Failed to save program"); _logger.Error(ex, "Failed to save program");
} }
} }
/// <summary>
/// 从文件加载程序 | Load program from file
/// </summary>
private async Task ExecuteLoadProgramAsync() private async Task ExecuteLoadProgramAsync()
{ {
try try
@@ -318,35 +252,27 @@ namespace XplorePlane.ViewModels.Cnc
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
IsModified = false; IsModified = false;
RefreshNodes(); RefreshNodes();
_logger.Info("程序已加载 | Program loaded: {ProgramName}", _currentProgram.Name);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "加载程序失败 | Failed to load program"); _logger.Error(ex, "Failed to load program");
} }
} }
/// <summary>
/// 创建新程序 | Create a new program
/// </summary>
private void ExecuteNewProgram() private void ExecuteNewProgram()
{ {
var name = string.IsNullOrWhiteSpace(ProgramName) ? "新程序" : ProgramName; var name = string.IsNullOrWhiteSpace(ProgramName) ? "NewCncProgram" : ProgramName;
_currentProgram = _cncProgramService.CreateProgram(name); _currentProgram = _cncProgramService.CreateProgram(name);
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
IsModified = false; IsModified = false;
RefreshNodes(); RefreshNodes();
_logger.Info("已创建新程序 | Created new program: {ProgramName}", name);
} }
/// <summary>
/// 导出当前程序为 CSV 文件 | Export current program to CSV file
/// </summary>
private void ExecuteExportCsv() private void ExecuteExportCsv()
{ {
if (_currentProgram == null || _currentProgram.Nodes.Count == 0) if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
{ {
_logger.Warn("无法导出 CSV:当前无程序或节点为空 | Cannot export CSV: no program or empty nodes"); _logger.Warn("Cannot export CSV: no program or empty nodes");
return; return;
} }
@@ -364,8 +290,7 @@ namespace XplorePlane.ViewModels.Cnc
return; return;
var sb = new StringBuilder(); var sb = new StringBuilder();
// CSV 表头 | CSV header sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Current_uA,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
var inv = CultureInfo.InvariantCulture; var inv = CultureInfo.InvariantCulture;
@@ -373,24 +298,15 @@ namespace XplorePlane.ViewModels.Cnc
{ {
var row = node switch var row = node switch
{ {
ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},,,,,,,,,,,,,,", ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},{rp.Voltage.ToString(inv)},{rp.Current.ToString(inv)},,{rp.IsRayOn},,,,,,,,,,",
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},,{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,",
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,", SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},,{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,", SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,",
InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}", InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,", InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,", PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},", WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,", CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,",
_ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,," _ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,"
}; };
@@ -398,18 +314,13 @@ namespace XplorePlane.ViewModels.Cnc
} }
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8); File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
_logger.Info("CSV 已导出 | CSV exported: {FilePath}, 节点数={Count}", dlg.FileName, _currentProgram.Nodes.Count);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "导出 CSV 失败 | Failed to export CSV"); _logger.Error(ex, "Failed to export CSV");
} }
} }
/// <summary>
/// CSV 字段转义:含逗号、引号或换行时用双引号包裹
/// Escape CSV field: wrap with double quotes if it contains comma, quote, or newline
/// </summary>
private static string Esc(string value) private static string Esc(string value)
{ {
if (string.IsNullOrEmpty(value)) return string.Empty; if (string.IsNullOrEmpty(value)) return string.Empty;
@@ -418,12 +329,6 @@ namespace XplorePlane.ViewModels.Cnc
return value; return value;
} }
// ── 辅助方法 | Helper methods ───────────────────────────────────
/// <summary>
/// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件
/// Unified post-edit handling: refresh nodes, mark modified, publish change event
/// </summary>
private void OnProgramEdited() private void OnProgramEdited()
{ {
IsModified = true; IsModified = true;
@@ -431,34 +336,70 @@ namespace XplorePlane.ViewModels.Cnc
PublishProgramChanged(); PublishProgramChanged();
} }
/// <summary> private void HandleNodeModelChanged(CncNodeViewModel nodeVm, CncNode updatedNode)
/// 从 _currentProgram.Nodes 重建 Nodes 集合
/// Rebuild the Nodes collection from _currentProgram.Nodes
/// </summary>
private void RefreshNodes()
{ {
Nodes.Clear(); if (_currentProgram == null)
if (_currentProgram?.Nodes == null)
return; return;
foreach (var node in _currentProgram.Nodes) _currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode);
{ IsModified = true;
Nodes.Add(new CncNodeViewModel(node)); ProgramName = _currentProgram.Name;
} PublishProgramChanged();
}
// 尝试保持选中状态 | Try to preserve selection
if (SelectedNode != null) private void RefreshNodes()
{ {
var match = Nodes.FirstOrDefault(n => n.Index == SelectedNode.Index); var selectedId = SelectedNode?.Id;
SelectedNode = match ?? Nodes.LastOrDefault(); var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
}
var flatNodes = new List<CncNodeViewModel>();
var rootNodes = new List<CncNodeViewModel>();
CncNodeViewModel currentModule = null;
if (_currentProgram?.Nodes != null)
{
foreach (var node in _currentProgram.Nodes)
{
var vm = new CncNodeViewModel(node, HandleNodeModelChanged)
{
IsExpanded = expansionState.TryGetValue(node.Id, out var isExpanded) ? isExpanded : true
};
flatNodes.Add(vm);
if (vm.IsInspectionModule)
{
rootNodes.Add(vm);
currentModule = vm;
continue;
}
if (currentModule != null && IsModuleChild(vm.NodeType))
{
currentModule.Children.Add(vm);
continue;
}
rootNodes.Add(vm);
currentModule = null;
}
}
Nodes = new ObservableCollection<CncNodeViewModel>(flatNodes);
TreeNodes = new ObservableCollection<CncNodeViewModel>(rootNodes);
SelectedNode = selectedId.HasValue
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
: Nodes.LastOrDefault();
}
private static bool IsModuleChild(CncNodeType type)
{
return type is CncNodeType.InspectionMarker
or CncNodeType.PauseDialog
or CncNodeType.WaitDelay;
} }
/// <summary>
/// 通过 IEventAggregator 发布 CNC 程序变更事件
/// Publish CNC program changed event via IEventAggregator
/// </summary>
private void PublishProgramChanged() private void PublishProgramChanged()
{ {
_eventAggregator _eventAggregator
@@ -466,4 +407,4 @@ namespace XplorePlane.ViewModels.Cnc
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified)); .Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
} }
} }
} }
+504 -50
View File
@@ -1,89 +1,543 @@
using Prism.Mvvm; using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using XplorePlane.Models; using XplorePlane.Models;
namespace XplorePlane.ViewModels.Cnc namespace XplorePlane.ViewModels.Cnc
{ {
/// <summary> /// <summary>
/// CNC 节点 ViewModel,将 CncNode 模型封装为可绑定的 WPF ViewModel /// CNC node ViewModel with editable properties and tree children.
/// CNC node ViewModel that wraps a CncNode model into a bindable WPF ViewModel
/// </summary> /// </summary>
public class CncNodeViewModel : BindableBase public class CncNodeViewModel : BindableBase
{ {
private int _index; private readonly Action<CncNodeViewModel, CncNode> _modelChangedCallback;
private string _name; private CncNode _model;
private CncNodeType _nodeType;
private string _icon; private string _icon;
private bool _isExpanded = true;
/// <summary> public CncNodeViewModel(CncNode model, Action<CncNodeViewModel, CncNode> modelChangedCallback)
/// 构造函数,从 CncNode 模型初始化 ViewModel
/// Constructor that initializes the ViewModel from a CncNode model
/// </summary>
public CncNodeViewModel(CncNode model)
{ {
Model = model; _model = model ?? throw new ArgumentNullException(nameof(model));
_index = model.Index; _modelChangedCallback = modelChangedCallback ?? throw new ArgumentNullException(nameof(modelChangedCallback));
_name = model.Name;
_nodeType = model.NodeType;
_icon = GetIconForNodeType(model.NodeType); _icon = GetIconForNodeType(model.NodeType);
Children = new ObservableCollection<CncNodeViewModel>();
} }
/// <summary>底层 CNC 节点模型(只读)| Underlying CNC node model (read-only)</summary> public ObservableCollection<CncNodeViewModel> Children { get; }
public CncNode Model { get; }
/// <summary>节点在程序中的索引 | Node index in the program</summary> public CncNode Model => _model;
public int Index
{ public Guid Id => _model.Id;
get => _index;
set => SetProperty(ref _index, value); public int Index => _model.Index;
}
/// <summary>节点显示名称 | Node display name</summary>
public string Name public string Name
{ {
get => _name; get => _model.Name;
set => SetProperty(ref _name, value); set => UpdateModel(_model with { Name = value ?? string.Empty });
} }
/// <summary>节点类型 | Node type</summary> public CncNodeType NodeType => _model.NodeType;
public CncNodeType NodeType
public string NodeTypeDisplay => NodeType.ToString();
public string Icon
{ {
get => _nodeType; get => _icon;
private set => SetProperty(ref _icon, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public bool HasChildren => Children.Count > 0;
public bool IsReferencePoint => _model is ReferencePointNode;
public bool IsSaveNode => _model is SaveNodeNode;
public bool IsSaveNodeWithImage => _model is SaveNodeWithImageNode;
public bool IsSavePosition => _model is SavePositionNode;
public bool IsInspectionModule => _model is InspectionModuleNode;
public bool IsInspectionMarker => _model is InspectionMarkerNode;
public bool IsPauseDialog => _model is PauseDialogNode;
public bool IsWaitDelay => _model is WaitDelayNode;
public bool IsCompleteProgram => _model is CompleteProgramNode;
public bool IsMotionSnapshotNode => _model is ReferencePointNode or SaveNodeNode or SaveNodeWithImageNode or SavePositionNode;
public double XM
{
get => _model switch
{
ReferencePointNode rp => rp.XM,
SaveNodeNode sn => sn.MotionState.XM,
SaveNodeWithImageNode sni => sni.MotionState.XM,
SavePositionNode sp => sp.MotionState.XM,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.XM);
}
public double YM
{
get => _model switch
{
ReferencePointNode rp => rp.YM,
SaveNodeNode sn => sn.MotionState.YM,
SaveNodeWithImageNode sni => sni.MotionState.YM,
SavePositionNode sp => sp.MotionState.YM,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.YM);
}
public double ZT
{
get => _model switch
{
ReferencePointNode rp => rp.ZT,
SaveNodeNode sn => sn.MotionState.ZT,
SaveNodeWithImageNode sni => sni.MotionState.ZT,
SavePositionNode sp => sp.MotionState.ZT,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.ZT);
}
public double ZD
{
get => _model switch
{
ReferencePointNode rp => rp.ZD,
SaveNodeNode sn => sn.MotionState.ZD,
SaveNodeWithImageNode sni => sni.MotionState.ZD,
SavePositionNode sp => sp.MotionState.ZD,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.ZD);
}
public double TiltD
{
get => _model switch
{
ReferencePointNode rp => rp.TiltD,
SaveNodeNode sn => sn.MotionState.TiltD,
SaveNodeWithImageNode sni => sni.MotionState.TiltD,
SavePositionNode sp => sp.MotionState.TiltD,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.TiltD);
}
public double Dist
{
get => _model switch
{
ReferencePointNode rp => rp.Dist,
SaveNodeNode sn => sn.MotionState.Dist,
SaveNodeWithImageNode sni => sni.MotionState.Dist,
SavePositionNode sp => sp.MotionState.Dist,
_ => 0d
};
set => UpdateMotion(value, MotionAxis.Dist);
}
public bool IsRayOn
{
get => _model switch
{
ReferencePointNode rp => rp.IsRayOn,
SaveNodeNode sn => sn.RaySourceState.IsOn,
SaveNodeWithImageNode sni => sni.RaySourceState.IsOn,
_ => false
};
set => UpdateRaySource(isOn: value);
}
public double Voltage
{
get => _model switch
{
ReferencePointNode rp => rp.Voltage,
SaveNodeNode sn => sn.RaySourceState.Voltage,
SaveNodeWithImageNode sni => sni.RaySourceState.Voltage,
_ => 0d
};
set => UpdateRaySource(voltage: value);
}
public double Current
{
get => _model is ReferencePointNode rp ? rp.Current : 0d;
set set
{ {
if (SetProperty(ref _nodeType, value)) if (_model is ReferencePointNode rp)
{ {
// 类型变更时自动更新图标 | Auto-update icon when type changes UpdateModel(rp with { Current = value });
Icon = GetIconForNodeType(value);
} }
} }
} }
/// <summary>节点图标路径 | Node icon path</summary> public double Power
public string Icon
{ {
get => _icon; get => _model switch
set => SetProperty(ref _icon, value); {
SaveNodeNode sn => sn.RaySourceState.Power,
SaveNodeWithImageNode sni => sni.RaySourceState.Power,
_ => 0d
};
set => UpdateRaySource(power: value);
}
public bool DetectorConnected
{
get => _model switch
{
SaveNodeNode sn => sn.DetectorState.IsConnected,
SaveNodeWithImageNode sni => sni.DetectorState.IsConnected,
_ => false
};
set => UpdateDetector(isConnected: value);
}
public bool DetectorAcquiring
{
get => _model switch
{
SaveNodeNode sn => sn.DetectorState.IsAcquiring,
SaveNodeWithImageNode sni => sni.DetectorState.IsAcquiring,
_ => false
};
set => UpdateDetector(isAcquiring: value);
}
public double FrameRate
{
get => _model switch
{
SaveNodeNode sn => sn.DetectorState.FrameRate,
SaveNodeWithImageNode sni => sni.DetectorState.FrameRate,
_ => 0d
};
set => UpdateDetector(frameRate: value);
}
public string Resolution
{
get => _model switch
{
SaveNodeNode sn => sn.DetectorState.Resolution,
SaveNodeWithImageNode sni => sni.DetectorState.Resolution,
_ => string.Empty
};
set => UpdateDetector(resolution: value ?? string.Empty);
}
public string ImageFileName
{
get => _model is SaveNodeWithImageNode sni ? sni.ImageFileName : string.Empty;
set
{
if (_model is SaveNodeWithImageNode sni)
{
UpdateModel(sni with { ImageFileName = value ?? string.Empty });
}
}
}
public string PipelineName
{
get => _model is InspectionModuleNode im ? im.Pipeline?.Name ?? string.Empty : string.Empty;
set
{
if (_model is InspectionModuleNode im)
{
var pipeline = im.Pipeline ?? new PipelineModel();
pipeline.Name = value ?? string.Empty;
UpdateModel(im with { Pipeline = pipeline });
}
}
}
public PipelineModel Pipeline
{
get => _model is InspectionModuleNode im ? im.Pipeline : null;
set
{
if (_model is InspectionModuleNode im)
{
UpdateModel(im with { Pipeline = value ?? new PipelineModel() });
}
}
}
public string MarkerType
{
get => _model is InspectionMarkerNode mk ? mk.MarkerType : string.Empty;
set
{
if (_model is InspectionMarkerNode mk)
{
UpdateModel(mk with { MarkerType = value ?? string.Empty });
}
}
}
public double MarkerX
{
get => _model is InspectionMarkerNode mk ? mk.MarkerX : 0d;
set
{
if (_model is InspectionMarkerNode mk)
{
UpdateModel(mk with { MarkerX = value });
}
}
}
public double MarkerY
{
get => _model is InspectionMarkerNode mk ? mk.MarkerY : 0d;
set
{
if (_model is InspectionMarkerNode mk)
{
UpdateModel(mk with { MarkerY = value });
}
}
}
public string DialogTitle
{
get => _model is PauseDialogNode pd ? pd.DialogTitle : string.Empty;
set
{
if (_model is PauseDialogNode pd)
{
UpdateModel(pd with { DialogTitle = value ?? string.Empty });
}
}
}
public string DialogMessage
{
get => _model is PauseDialogNode pd ? pd.DialogMessage : string.Empty;
set
{
if (_model is PauseDialogNode pd)
{
UpdateModel(pd with { DialogMessage = value ?? string.Empty });
}
}
}
public int DelayMilliseconds
{
get => _model is WaitDelayNode wd ? wd.DelayMilliseconds : 0;
set
{
if (_model is WaitDelayNode wd)
{
UpdateModel(wd with { DelayMilliseconds = value });
}
}
}
public void ReplaceModel(CncNode model)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
Icon = GetIconForNodeType(model.NodeType);
RaiseAllPropertiesChanged();
} }
/// <summary>
/// 根据节点类型返回对应的图标路径
/// Returns the icon path for the given node type
/// </summary>
public static string GetIconForNodeType(CncNodeType nodeType) public static string GetIconForNodeType(CncNodeType nodeType)
{ {
return nodeType switch return nodeType switch
{ {
CncNodeType.ReferencePoint => "/Resources/Icons/cnc_reference_point.png", CncNodeType.ReferencePoint => "/Assets/Icons/reference.png",
CncNodeType.SaveNodeWithImage => "/Resources/Icons/cnc_save_with_image.png", CncNodeType.SaveNodeWithImage => "/Assets/Icons/saveall.png",
CncNodeType.SaveNode => "/Resources/Icons/cnc_save_node.png", CncNodeType.SaveNode => "/Assets/Icons/save.png",
CncNodeType.SavePosition => "/Resources/Icons/cnc_save_position.png", CncNodeType.SavePosition => "/Assets/Icons/add-pos.png",
CncNodeType.InspectionModule => "/Resources/Icons/cnc_inspection_module.png", CncNodeType.InspectionModule => "/Assets/Icons/Module.png",
CncNodeType.InspectionMarker => "/Resources/Icons/cnc_inspection_marker.png", CncNodeType.InspectionMarker => "/Assets/Icons/mark.png",
CncNodeType.PauseDialog => "/Resources/Icons/cnc_pause_dialog.png", CncNodeType.PauseDialog => "/Assets/Icons/message.png",
CncNodeType.WaitDelay => "/Resources/Icons/cnc_wait_delay.png", CncNodeType.WaitDelay => "/Assets/Icons/wait.png",
CncNodeType.CompleteProgram => "/Resources/Icons/cnc_complete_program.png", CncNodeType.CompleteProgram => "/Assets/Icons/finish.png",
_ => "/Resources/Icons/cnc_default.png", _ => "/Assets/Icons/cnc.png",
}; };
} }
private void UpdateMotion(double value, MotionAxis axis)
{
switch (_model)
{
case ReferencePointNode rp:
UpdateModel(axis switch
{
MotionAxis.XM => rp with { XM = value },
MotionAxis.YM => rp with { YM = value },
MotionAxis.ZT => rp with { ZT = value },
MotionAxis.ZD => rp with { ZD = value },
MotionAxis.TiltD => rp with { TiltD = value },
MotionAxis.Dist => rp with { Dist = value },
_ => rp
});
break;
case SaveNodeNode sn:
UpdateModel(sn with { MotionState = UpdateMotionState(sn.MotionState, axis, value) });
break;
case SaveNodeWithImageNode sni:
UpdateModel(sni with { MotionState = UpdateMotionState(sni.MotionState, axis, value) });
break;
case SavePositionNode sp:
UpdateModel(sp with { MotionState = UpdateMotionState(sp.MotionState, axis, value) });
break;
}
}
private void UpdateRaySource(bool? isOn = null, double? voltage = null, double? current = null, double? power = null)
{
switch (_model)
{
case ReferencePointNode rp:
UpdateModel(rp with
{
IsRayOn = isOn ?? rp.IsRayOn,
Voltage = voltage ?? rp.Voltage,
Current = current ?? rp.Current
});
break;
case SaveNodeNode sn:
UpdateModel(sn with
{
RaySourceState = sn.RaySourceState with
{
IsOn = isOn ?? sn.RaySourceState.IsOn,
Voltage = voltage ?? sn.RaySourceState.Voltage,
Power = power ?? sn.RaySourceState.Power
}
});
break;
case SaveNodeWithImageNode sni:
UpdateModel(sni with
{
RaySourceState = sni.RaySourceState with
{
IsOn = isOn ?? sni.RaySourceState.IsOn,
Voltage = voltage ?? sni.RaySourceState.Voltage,
Power = power ?? sni.RaySourceState.Power
}
});
break;
}
}
private void UpdateDetector(bool? isConnected = null, bool? isAcquiring = null, double? frameRate = null, string resolution = null)
{
switch (_model)
{
case SaveNodeNode sn:
UpdateModel(sn with
{
DetectorState = sn.DetectorState with
{
IsConnected = isConnected ?? sn.DetectorState.IsConnected,
IsAcquiring = isAcquiring ?? sn.DetectorState.IsAcquiring,
FrameRate = frameRate ?? sn.DetectorState.FrameRate,
Resolution = resolution ?? sn.DetectorState.Resolution
}
});
break;
case SaveNodeWithImageNode sni:
UpdateModel(sni with
{
DetectorState = sni.DetectorState with
{
IsConnected = isConnected ?? sni.DetectorState.IsConnected,
IsAcquiring = isAcquiring ?? sni.DetectorState.IsAcquiring,
FrameRate = frameRate ?? sni.DetectorState.FrameRate,
Resolution = resolution ?? sni.DetectorState.Resolution
}
});
break;
}
}
private static MotionState UpdateMotionState(MotionState state, MotionAxis axis, double value)
{
return axis switch
{
MotionAxis.XM => state with { XM = value },
MotionAxis.YM => state with { YM = value },
MotionAxis.ZT => state with { ZT = value },
MotionAxis.ZD => state with { ZD = value },
MotionAxis.TiltD => state with { TiltD = value },
MotionAxis.Dist => state with { Dist = value },
_ => state
};
}
private void UpdateModel(CncNode updatedModel)
{
_model = updatedModel ?? throw new ArgumentNullException(nameof(updatedModel));
RaiseAllPropertiesChanged();
_modelChangedCallback(this, updatedModel);
}
private void RaiseAllPropertiesChanged()
{
RaisePropertyChanged(nameof(Model));
RaisePropertyChanged(nameof(Id));
RaisePropertyChanged(nameof(Index));
RaisePropertyChanged(nameof(Name));
RaisePropertyChanged(nameof(NodeType));
RaisePropertyChanged(nameof(NodeTypeDisplay));
RaisePropertyChanged(nameof(Icon));
RaisePropertyChanged(nameof(IsReferencePoint));
RaisePropertyChanged(nameof(IsSaveNode));
RaisePropertyChanged(nameof(IsSaveNodeWithImage));
RaisePropertyChanged(nameof(IsSavePosition));
RaisePropertyChanged(nameof(IsInspectionModule));
RaisePropertyChanged(nameof(IsInspectionMarker));
RaisePropertyChanged(nameof(IsPauseDialog));
RaisePropertyChanged(nameof(IsWaitDelay));
RaisePropertyChanged(nameof(IsCompleteProgram));
RaisePropertyChanged(nameof(IsMotionSnapshotNode));
RaisePropertyChanged(nameof(XM));
RaisePropertyChanged(nameof(YM));
RaisePropertyChanged(nameof(ZT));
RaisePropertyChanged(nameof(ZD));
RaisePropertyChanged(nameof(TiltD));
RaisePropertyChanged(nameof(Dist));
RaisePropertyChanged(nameof(IsRayOn));
RaisePropertyChanged(nameof(Voltage));
RaisePropertyChanged(nameof(Current));
RaisePropertyChanged(nameof(Power));
RaisePropertyChanged(nameof(DetectorConnected));
RaisePropertyChanged(nameof(DetectorAcquiring));
RaisePropertyChanged(nameof(FrameRate));
RaisePropertyChanged(nameof(Resolution));
RaisePropertyChanged(nameof(ImageFileName));
RaisePropertyChanged(nameof(Pipeline));
RaisePropertyChanged(nameof(PipelineName));
RaisePropertyChanged(nameof(MarkerType));
RaisePropertyChanged(nameof(MarkerX));
RaisePropertyChanged(nameof(MarkerY));
RaisePropertyChanged(nameof(DialogTitle));
RaisePropertyChanged(nameof(DialogMessage));
RaisePropertyChanged(nameof(DelayMilliseconds));
}
private enum MotionAxis
{
XM,
YM,
ZT,
ZD,
TiltD,
Dist
}
} }
} }
@@ -8,6 +8,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Events; using XplorePlane.Events;
@@ -16,7 +17,7 @@ using XplorePlane.Services;
namespace XplorePlane.ViewModels namespace XplorePlane.ViewModels
{ {
public class PipelineEditorViewModel : BindableBase public class PipelineEditorViewModel : BindableBase, IPipelineEditorHostViewModel
{ {
private const int MaxPipelineLength = 20; private const int MaxPipelineLength = 20;
private const int DebounceDelayMs = 300; private const int DebounceDelayMs = 300;
@@ -165,6 +166,15 @@ namespace XplorePlane.ViewModels
public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { get; } public DelegateCommand<PipelineNodeViewModel> MoveNodeUpCommand { get; }
public DelegateCommand<PipelineNodeViewModel> MoveNodeDownCommand { get; } public DelegateCommand<PipelineNodeViewModel> MoveNodeDownCommand { get; }
ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand;
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
ICommand IPipelineEditorHostViewModel.SavePipelineCommand => SavePipelineCommand;
ICommand IPipelineEditorHostViewModel.SaveAsPipelineCommand => SaveAsPipelineCommand;
ICommand IPipelineEditorHostViewModel.LoadPipelineCommand => LoadPipelineCommand;
// ── Command Implementations ─────────────────────────────────── // ── Command Implementations ───────────────────────────────────
private bool CanAddOperator(string operatorKey) => private bool CanAddOperator(string operatorKey) =>
+93 -2
View File
@@ -1,4 +1,4 @@
using Prism.Commands; using Prism.Commands;
using Prism.Events; using Prism.Events;
using Prism.Ioc; using Prism.Ioc;
using Prism.Mvvm; using Prism.Mvvm;
@@ -10,6 +10,9 @@ using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using XplorePlane.Events; using XplorePlane.Events;
using XplorePlane.ViewModels.Cnc;
using XplorePlane.Views;
using XplorePlane.Views.Cnc;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Common.PdfViewer.Interfaces; using XP.Common.PdfViewer.Interfaces;
using XP.Hardware.MotionControl.Abstractions; using XP.Hardware.MotionControl.Abstractions;
@@ -18,9 +21,12 @@ namespace XplorePlane.ViewModels
{ {
public class MainViewModel : BindableBase public class MainViewModel : BindableBase
{ {
private const double CncEditorHostWidth = 710d;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IContainerProvider _containerProvider; private readonly IContainerProvider _containerProvider;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly CncEditorViewModel _cncEditorViewModel;
private readonly CncPageView _cncPageView;
private string _licenseInfo = "当前时间"; private string _licenseInfo = "当前时间";
public string LicenseInfo public string LicenseInfo
@@ -49,6 +55,17 @@ namespace XplorePlane.ViewModels
public DelegateCommand OpenLibraryVersionsCommand { get; } public DelegateCommand OpenLibraryVersionsCommand { get; }
public DelegateCommand OpenUserManualCommand { get; } public DelegateCommand OpenUserManualCommand { get; }
public DelegateCommand OpenCameraSettingsCommand { get; } public DelegateCommand OpenCameraSettingsCommand { get; }
public DelegateCommand NewCncProgramCommand { get; }
public DelegateCommand SaveCncProgramCommand { get; }
public DelegateCommand LoadCncProgramCommand { get; }
public DelegateCommand InsertReferencePointCommand { get; }
public DelegateCommand InsertSavePositionCommand { get; }
public DelegateCommand InsertCompleteProgramCommand { get; }
public DelegateCommand InsertInspectionMarkerCommand { get; }
public DelegateCommand InsertInspectionModuleCommand { get; }
public DelegateCommand InsertSaveNodeCommand { get; }
public DelegateCommand InsertPauseDialogCommand { get; }
public DelegateCommand InsertWaitDelayCommand { get; }
// 硬件命令 // 硬件命令
public DelegateCommand AxisResetCommand { get; } public DelegateCommand AxisResetCommand { get; }
@@ -62,6 +79,27 @@ namespace XplorePlane.ViewModels
public DelegateCommand OpenLanguageSwitcherCommand { get; } public DelegateCommand OpenLanguageSwitcherCommand { get; }
public DelegateCommand OpenRealTimeLogViewerCommand { get; } public DelegateCommand OpenRealTimeLogViewerCommand { get; }
/// <summary>右侧图像区域内容 | Right-side image panel content</summary>
public object ImagePanelContent
{
get => _imagePanelContent;
set => SetProperty(ref _imagePanelContent, value);
}
/// <summary>右侧图像区域宽度 | Right-side image panel width</summary>
public GridLength ImagePanelWidth
{
get => _imagePanelWidth;
set => SetProperty(ref _imagePanelWidth, value);
}
/// <summary>主视图区宽度 | Main viewport width</summary>
public GridLength ViewportPanelWidth
{
get => _viewportPanelWidth;
set => SetProperty(ref _viewportPanelWidth, value);
}
// 窗口引用(单例窗口防止重复打开) // 窗口引用(单例窗口防止重复打开)
private Window _motionDebugWindow; private Window _motionDebugWindow;
private Window _detectorConfigWindow; private Window _detectorConfigWindow;
@@ -69,12 +107,18 @@ namespace XplorePlane.ViewModels
private Window _realTimeLogViewerWindow; private Window _realTimeLogViewerWindow;
private Window _toolboxWindow; private Window _toolboxWindow;
private Window _raySourceConfigWindow; private Window _raySourceConfigWindow;
private object _imagePanelContent;
private GridLength _viewportPanelWidth = new GridLength(1, GridUnitType.Star);
private GridLength _imagePanelWidth = new GridLength(320);
private bool _isCncEditorMode;
public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator) public MainViewModel(ILoggerService logger, IContainerProvider containerProvider, IEventAggregator eventAggregator)
{ {
_logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger)); _logger = logger?.ForModule<MainViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_cncEditorViewModel = _containerProvider.Resolve<CncEditorViewModel>();
_cncPageView = new CncPageView { DataContext = _cncEditorViewModel };
NavigationTree = new ObservableCollection<object>(); NavigationTree = new ObservableCollection<object>();
@@ -90,12 +134,23 @@ namespace XplorePlane.ViewModels
OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理")); OpenImageProcessingCommand = new DelegateCommand(() => ShowWindow(new Views.ImageProcessingWindow(), "图像处理"));
LoadImageCommand = new DelegateCommand(ExecuteLoadImage); LoadImageCommand = new DelegateCommand(ExecuteLoadImage);
OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器")); OpenPipelineEditorCommand = new DelegateCommand(() => ShowWindow(new Views.PipelineEditorWindow(), "流水线编辑器"));
OpenCncEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.CncEditorWindow(), "CNC 编辑器")); OpenCncEditorCommand = new DelegateCommand(ExecuteOpenCncEditor);
OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排")); OpenMatrixEditorCommand = new DelegateCommand(() => ShowWindow(new Views.Cnc.MatrixEditorWindow(), "矩阵编排"));
OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox); OpenToolboxCommand = new DelegateCommand(ExecuteOpenToolbox);
OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于")); OpenLibraryVersionsCommand = new DelegateCommand(() => ShowWindow(new Views.LibraryVersionsWindow(), "关于"));
OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual); OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual);
OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings); OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings);
NewCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.NewProgramCommand.Execute()));
SaveCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.SaveProgramCommand.Execute()));
LoadCncProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.LoadProgramCommand.Execute()));
InsertReferencePointCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertReferencePointCommand.Execute()));
InsertSavePositionCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSavePositionCommand.Execute()));
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertCompleteProgramCommand.Execute()));
InsertInspectionMarkerCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionMarkerCommand.Execute()));
InsertInspectionModuleCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertInspectionModuleCommand.Execute()));
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertSaveNodeCommand.Execute()));
InsertPauseDialogCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertPauseDialogCommand.Execute()));
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteCncEditorAction(vm => vm.InsertWaitDelayCommand.Execute()));
// 硬件命令 // 硬件命令
AxisResetCommand = new DelegateCommand(ExecuteAxisReset); AxisResetCommand = new DelegateCommand(ExecuteAxisReset);
@@ -109,6 +164,10 @@ namespace XplorePlane.ViewModels
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher); OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer); OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer);
ImagePanelContent = new PipelineEditorView();
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
ImagePanelWidth = new GridLength(320);
_logger.Info("MainViewModel 已初始化"); _logger.Info("MainViewModel 已初始化");
} }
@@ -153,6 +212,38 @@ namespace XplorePlane.ViewModels
ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱"); ShowOrActivate(_toolboxWindow, w => _toolboxWindow = w, () => new Views.OperatorToolboxWindow(), "算子工具箱");
} }
private void ExecuteOpenCncEditor()
{
if (_isCncEditorMode)
{
ImagePanelContent = new PipelineEditorView();
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
ImagePanelWidth = new GridLength(320);
_isCncEditorMode = false;
_logger.Info("已退出 CNC 编辑模式");
return;
}
ShowCncEditor();
}
private void ExecuteCncEditorAction(Action<CncEditorViewModel> action)
{
ArgumentNullException.ThrowIfNull(action);
ShowCncEditor();
action(_cncEditorViewModel);
}
private void ShowCncEditor()
{
ImagePanelContent = _cncPageView;
ViewportPanelWidth = new GridLength(1, GridUnitType.Star);
ImagePanelWidth = new GridLength(CncEditorHostWidth);
_isCncEditorMode = true;
_logger.Info("CNC 编辑器已切换到主界面图像区域");
}
private void ExecuteOpenUserManual() private void ExecuteOpenUserManual()
{ {
try try
+7 -4
View File
@@ -1,12 +1,15 @@
<Window <Window
x:Class="XplorePlane.Views.Cnc.CncEditorWindow" x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc" xmlns:cnc="clr-namespace:XplorePlane.Views.Cnc"
Title="CNC 编辑器" Title="CNC 编辑器"
Width="350" Width="1040"
Height="750" Height="780"
MinWidth="960"
MinHeight="720"
ResizeMode="CanResize"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"> WindowStartupLocation="CenterOwner">
<cnc:CncPageView /> <cnc:CncPageView />
</Window> </Window>
+360 -200
View File
@@ -1,297 +1,457 @@
<!-- CNC 编辑器主页面视图 | CNC editor main page view -->
<UserControl <UserControl
x:Class="XplorePlane.Views.Cnc.CncPageView" x:Class="XplorePlane.Views.Cnc.CncPageView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:XplorePlane.Views.Cnc"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
d:DesignHeight="700" xmlns:views="clr-namespace:XplorePlane.Views"
d:DesignWidth="350" xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
d:DesignHeight="760"
d:DesignWidth="702"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<!-- 面板背景和边框颜色 | Panel background and border colors --> <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<SolidColorBrush x:Key="PanelBg" Color="White" /> <local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<!-- 节点列表项样式 | Node list item style --> <SolidColorBrush x:Key="PanelBg" Color="White" />
<Style x:Key="CncNodeItemStyle" TargetType="ListBoxItem"> <SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
<SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" />
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="Padding" Value="0" /> <Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style> </Style>
<!-- 工具栏按钮样式 | Toolbar button style --> <Style x:Key="EditorTitle" TargetType="TextBlock">
<Style x:Key="ToolbarBtn" TargetType="Button"> <Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="Height" Value="28" /> <Setter Property="FontSize" Value="13" />
<Setter Property="Margin" Value="2,0" /> <Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="6,0" /> <Setter Property="Margin" Value="0,0,0,8" />
<Setter Property="Background" Value="Transparent" /> </Style>
<Setter Property="BorderBrush" Value="#cdcbcb" />
<Setter Property="BorderThickness" Value="1" /> <Style x:Key="LabelStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" /> <Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="#666666" />
<Setter Property="Margin" Value="0,0,0,3" />
</Style>
<Style x:Key="EditorBox" TargetType="TextBox">
<Setter Property="Height" Value="28" />
<Setter Property="Padding" Value="8,3" />
<Setter Property="Margin" Value="0,0,0,8" />
<Setter Property="BorderBrush" Value="#CFCFCF" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" />
</Style>
<Style x:Key="EditorCheck" TargetType="CheckBox">
<Setter Property="Margin" Value="0,2,0,8" />
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" />
</Style>
<Style x:Key="CompactGroupBox" TargetType="GroupBox">
<Setter Property="Margin" Value="0,0,0,10" />
<Setter Property="Padding" Value="0" />
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
<Setter Property="Cursor" Value="Hand" />
</Style> </Style>
</UserControl.Resources> </UserControl.Resources>
<Border <Border
Width="702"
MinWidth="702"
HorizontalAlignment="Left"
Background="{StaticResource PanelBg}" Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}" BorderBrush="{StaticResource PanelBorder}"
BorderThickness="1" BorderThickness="1"
CornerRadius="4"> CornerRadius="4">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.ColumnDefinitions>
<!-- Row 0: 工具栏 | Toolbar --> <ColumnDefinition Width="200" />
<RowDefinition Height="Auto" /> <ColumnDefinition Width="1" />
<!-- Row 1: 主内容区(左侧节点列表 + 右侧参数面板)| Main content (left: node list, right: parameter panel) --> <ColumnDefinition Width="250" />
<RowDefinition Height="*" /> <ColumnDefinition Width="1" />
</Grid.RowDefinitions> <ColumnDefinition Width="250" />
</Grid.ColumnDefinitions>
<!-- ═══ 工具栏:节点插入命令 + 文件操作命令 | Toolbar: node insert commands + file operation commands ═══ --> <Grid Grid.Column="0">
<Border <Grid.RowDefinitions>
Grid.Row="0" <RowDefinition Height="Auto" />
Padding="6,4" <RowDefinition Height="*" />
Background="#F5F5F5" </Grid.RowDefinitions>
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="0,0,0,1">
<WrapPanel Orientation="Horizontal">
<!-- 文件操作按钮 | File operation buttons -->
<Button
Command="{Binding NewProgramCommand}"
Content="新建"
Style="{StaticResource ToolbarBtn}"
ToolTip="新建程序 | New Program" />
<Button
Command="{Binding SaveProgramCommand}"
Content="保存"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存程序 | Save Program" />
<Button
Command="{Binding LoadProgramCommand}"
Content="加载"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载程序 | Load Program" />
<Button
Command="{Binding ExportCsvCommand}"
Content="导出CSV"
Style="{StaticResource ToolbarBtn}"
ToolTip="导出 CSV | Export CSV" />
<!-- 分隔线 | Separator --> <Border
<Rectangle Grid.Row="0"
Width="1" Padding="10,8"
Height="20" Background="{StaticResource HeaderBg}"
Margin="4,0" BorderBrush="{StaticResource SeparatorBrush}"
Fill="{StaticResource SeparatorBrush}" /> BorderThickness="0,0,0,1">
<StackPanel>
<TextBlock
FontFamily="{StaticResource UiFont}"
FontSize="13"
FontWeight="SemiBold"
Text="{Binding ProgramName, TargetNullValue=CNC编辑}"
TextWrapping="Wrap" />
<TextBlock
Margin="0,3,0,0"
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#666666"
Text="模块节点下会自动显示标记、等待、消息等子节点。"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- 节点插入按钮(9 种节点类型)| Node insert buttons (9 node types) --> <TreeView
<Button x:Name="CncTreeView"
Command="{Binding InsertReferencePointCommand}" Grid.Row="1"
Content="参考点" Padding="4,6"
Style="{StaticResource ToolbarBtn}"
ToolTip="插入参考点 | Insert Reference Point" />
<Button
Command="{Binding InsertSaveNodeWithImageCommand}"
Content="保存+图"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存节点并保存图片 | Save Node With Image" />
<Button
Command="{Binding InsertSaveNodeCommand}"
Content="保存节点"
Style="{StaticResource ToolbarBtn}"
ToolTip="仅保存节点 | Save Node" />
<Button
Command="{Binding InsertSavePositionCommand}"
Content="保存位置"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存位置 | Save Position" />
<Button
Command="{Binding InsertInspectionModuleCommand}"
Content="检测模块"
Style="{StaticResource ToolbarBtn}"
ToolTip="插入检测模块 | Insert Inspection Module" />
<Button
Command="{Binding InsertInspectionMarkerCommand}"
Content="检测标记"
Style="{StaticResource ToolbarBtn}"
ToolTip="插入检测标记 | Insert Inspection Marker" />
<Button
Command="{Binding InsertPauseDialogCommand}"
Content="停顿"
Style="{StaticResource ToolbarBtn}"
ToolTip="插入停顿对话框 | Insert Pause Dialog" />
<Button
Command="{Binding InsertWaitDelayCommand}"
Content="延时"
Style="{StaticResource ToolbarBtn}"
ToolTip="设置等待延时 | Insert Wait Delay" />
<Button
Command="{Binding InsertCompleteProgramCommand}"
Content="完成"
Style="{StaticResource ToolbarBtn}"
ToolTip="完成程序 | Complete Program" />
</WrapPanel>
</Border>
<!-- ═══ 主内容区:左侧节点列表 + 右侧参数面板 | Main content: left node list + right parameter panel ═══ -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<!-- 左侧:节点列表 | Left: node list -->
<ColumnDefinition Width="3*" MinWidth="150" />
<!-- 分隔线 | Splitter -->
<ColumnDefinition Width="Auto" />
<!-- 右侧:参数面板 | Right: parameter panel -->
<ColumnDefinition Width="2*" MinWidth="150" />
</Grid.ColumnDefinitions>
<!-- ── 左侧:CNC 节点列表 | Left: CNC node list ── -->
<ListBox
x:Name="CncNodeListBox"
Grid.Column="0"
Background="Transparent" Background="Transparent"
BorderThickness="0" BorderThickness="0"
ItemContainerStyle="{StaticResource CncNodeItemStyle}" ItemsSource="{Binding TreeNodes}"
ItemsSource="{Binding Nodes}" SelectedItemChanged="CncTreeView_SelectedItemChanged">
ScrollViewer.HorizontalScrollBarVisibility="Disabled" <TreeView.Resources>
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"> <HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}">
<ListBox.ItemTemplate> <Grid x:Name="NodeRoot" MinHeight="34">
<DataTemplate>
<Grid x:Name="NodeRoot" MinHeight="40">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<!-- 图标列 | Icon column --> <ColumnDefinition Width="30" />
<ColumnDefinition Width="40" />
<!-- 名称列 | Name column -->
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<!-- 操作按钮列 | Action buttons column -->
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 节点图标 | Node icon -->
<Border <Border
Grid.Column="0" Grid.Column="0"
Width="28" Width="22"
Height="28" Height="22"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="#E8F0FE" Background="Transparent"
BorderBrush="#5B9BD5"
BorderThickness="1.5"
CornerRadius="4"> CornerRadius="4">
<Image <Image
Width="20" Width="15"
Height="20" Height="15"
Source="{Binding Icon}" Source="{Binding Icon}"
Stretch="Uniform" /> Stretch="Uniform" />
</Border> </Border>
<!-- 节点序号和名称 | Node index and name -->
<StackPanel <StackPanel
Grid.Column="1" Grid.Column="1"
Margin="6,0,0,0" Margin="4,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Orientation="Horizontal"> Orientation="Horizontal">
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI" FontFamily="{StaticResource UiFont}"
FontSize="11" FontSize="10.5"
Foreground="#888" Foreground="#888888"
Text="{Binding Index, StringFormat='[{0}] '}" /> Text="{Binding Index, StringFormat='[{0}] '}" />
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI" FontFamily="{StaticResource UiFont}"
FontSize="12" FontSize="11.5"
Text="{Binding Name}" /> Text="{Binding Name}" />
</StackPanel> </StackPanel>
<!-- 悬停操作按钮:上移 / 下移 / 删除 | Hover actions: MoveUp / MoveDown / Delete -->
<StackPanel <StackPanel
x:Name="NodeActions" x:Name="NodeActions"
Grid.Column="2" Grid.Column="2"
Margin="0,0,4,0" Margin="0,0,2,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Orientation="Horizontal" Orientation="Horizontal"
Visibility="Collapsed"> Visibility="Collapsed">
<Button <Button
Width="22" Width="20"
Height="22" Height="20"
Margin="1,0" Margin="1,0"
Background="Transparent" Background="White"
BorderBrush="#cdcbcb" BorderBrush="#CDCBCB"
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="" Content=""
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="上移 | Move Up" /> ToolTip="上移" />
<Button <Button
Width="22" Width="20"
Height="22" Height="20"
Margin="1,0" Margin="1,0"
Background="Transparent" Background="White"
BorderBrush="#cdcbcb" BorderBrush="#CDCBCB"
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="" Content=""
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="下移 | Move Down" /> ToolTip="下移" />
<Button <Button
Width="22" Width="20"
Height="22" Height="20"
Margin="1,0" Margin="1,0"
Background="Transparent" Background="White"
BorderBrush="#E05050" BorderBrush="#E05050"
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
Content="" Content=""
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="删除 | Delete" /> ToolTip="删除" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<DataTemplate.Triggers> <DataTemplate.Triggers>
<!-- 鼠标悬停时显示操作按钮 | Show action buttons on mouse hover -->
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True"> <Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" /> <Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
</Trigger> </Trigger>
</DataTemplate.Triggers> </DataTemplate.Triggers>
</DataTemplate> </HierarchicalDataTemplate>
</ListBox.ItemTemplate> </TreeView.Resources>
</ListBox> <TreeView.ItemContainerStyle>
<Style BasedOn="{StaticResource TreeItemStyle}" TargetType="TreeViewItem" />
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
<!-- 垂直分隔线 | Vertical separator --> <Rectangle
<Rectangle Grid.Column="1"
Grid.Column="1" Width="1"
Width="1" Fill="{StaticResource SeparatorBrush}" />
Fill="{StaticResource SeparatorBrush}" />
<!-- ── 右侧:参数面板(根据节点类型动态渲染)| Right: parameter panel (dynamic rendering by node type) ── --> <ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto">
<ScrollViewer <Grid Margin="10">
Grid.Column="2" <StackPanel Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}}">
HorizontalScrollBarVisibility="Disabled" <TextBlock Style="{StaticResource EditorTitle}" Text="节点属性" />
VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8,6">
<TextBlock
Margin="0,0,0,4"
FontFamily="{StaticResource CsdFont}"
FontSize="11"
FontWeight="Bold"
Foreground="#555"
Text="参数配置" />
<!-- 动态参数内容区域(占位:根据 SelectedNode 类型渲染)| Dynamic parameter content area (placeholder for node-type-based rendering) --> <TextBlock Style="{StaticResource LabelStyle}" Text="名称" />
<ContentControl Content="{Binding SelectedNode}" /> <TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Name, UpdateSourceTrigger=PropertyChanged}" />
<UniformGrid Columns="2" Margin="0,0,0,8">
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="索引" />
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.Index, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="类型" />
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.NodeTypeDisplay, Mode=OneWay}" />
</StackPanel>
</UniformGrid>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="运动参数"
Visibility="{Binding SelectedNode.IsMotionSnapshotNode, Converter={StaticResource BoolToVisibilityConverter}}">
<UniformGrid Margin="8,8,8,6" Columns="2">
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="XM" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.XM, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="YM" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.YM, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="ZT" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZT, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="ZD" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZD, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="TiltD" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.TiltD, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="Dist" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Dist, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
</UniformGrid>
</GroupBox>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="射线源"
Visibility="{Binding SelectedNode.IsReferencePoint, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6">
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="电流 (uA)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Current, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
</GroupBox>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="保存参数"
Visibility="{Binding SelectedNode.IsSaveNode, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6">
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</GroupBox>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="保存参数"
Visibility="{Binding SelectedNode.IsSaveNodeWithImage, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6">
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="图像文件" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ImageFileName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</GroupBox>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="检测模块"
Visibility="{Binding SelectedNode.IsInspectionModule, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="Pipeline 名称" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.PipelineName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</GroupBox>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="标记参数"
Visibility="{Binding SelectedNode.IsInspectionMarker, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="标记类型" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerType, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerX" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerX, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerY" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerY, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
</GroupBox>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="消息弹窗"
Visibility="{Binding SelectedNode.IsPauseDialog, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="标题" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DialogTitle, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="消息内容" />
<TextBox
MinHeight="68"
Margin="0,0,0,8"
Padding="8,6"
AcceptsReturn="True"
BorderBrush="#CFCFCF"
BorderThickness="1"
FontFamily="{StaticResource UiFont}"
FontSize="11"
Text="{Binding SelectedNode.DialogMessage, UpdateSourceTrigger=PropertyChanged}"
TextWrapping="Wrap" />
</StackPanel>
</GroupBox>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="等待参数"
Visibility="{Binding SelectedNode.IsWaitDelay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="10,8,10,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="延时 (ms)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DelayMilliseconds, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
</GroupBox>
</StackPanel> </StackPanel>
</ScrollViewer>
<Border
Padding="12"
Background="#FAFAFA"
BorderBrush="#E6E6E6"
BorderThickness="1"
CornerRadius="6"
Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Invert}">
<StackPanel>
<TextBlock
FontFamily="{StaticResource UiFont}"
FontSize="13"
FontWeight="SemiBold"
Text="未选择节点"
TextWrapping="Wrap" />
<TextBlock
Margin="0,6,0,0"
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#666666"
Text="从左侧树中选择一个节点后,这里会显示可编辑的参数。"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid>
</ScrollViewer>
<Rectangle
Grid.Column="3"
Width="1"
Fill="{StaticResource SeparatorBrush}" />
<Grid Grid.Column="4">
<views:PipelineEditorView
x:Name="InspectionModulePipelineEditor"
Visibility="{Binding EditorVisibility}" />
<Border
x:Name="InspectionModulePipelineEmptyState"
Margin="12"
Padding="16"
Background="#FAFAFA"
BorderBrush="#E6E6E6"
BorderThickness="1"
CornerRadius="6"
Visibility="{Binding EmptyStateVisibility}">
<StackPanel>
<TextBlock
FontFamily="{StaticResource UiFont}"
FontSize="13"
FontWeight="SemiBold"
Text="未选择检测模块"
TextWrapping="Wrap" />
<TextBlock
Margin="0,6,0,0"
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#666666"
Text="请选择一个检测模块节点后,在这里拖拽算子并配置参数。"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid> </Grid>
</Grid> </Grid>
</Border> </Border>
</UserControl> </UserControl>
+81 -3
View File
@@ -1,16 +1,94 @@
using Prism.Ioc;
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services;
using XplorePlane.ViewModels.Cnc;
namespace XplorePlane.Views.Cnc namespace XplorePlane.Views.Cnc
{ {
/// <summary> /// <summary>
/// CNC 编辑器主页面视图(MVVM 模式,逻辑在 ViewModel 中) /// CNC editor main page view.
/// CNC editor main page view (MVVM pattern, logic in ViewModel)
/// </summary> /// </summary>
public partial class CncPageView : UserControl public partial class CncPageView : UserControl
{ {
private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel;
public CncPageView() public CncPageView()
{ {
InitializeComponent(); InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
CncEditorViewModel editorViewModel = DataContext as CncEditorViewModel;
if (editorViewModel == null)
{
try
{
editorViewModel = ContainerLocator.Current?.Resolve<CncEditorViewModel>();
DataContext = editorViewModel;
}
catch (Exception)
{
// keep existing DataContext if resolution fails
}
}
if (editorViewModel == null || _inspectionModulePipelineViewModel != null)
return;
try
{
var imageProcessingService = ContainerLocator.Current.Resolve<IImageProcessingService>();
var persistenceService = ContainerLocator.Current.Resolve<IPipelinePersistenceService>();
var logger = ContainerLocator.Current.Resolve<ILoggerService>();
_inspectionModulePipelineViewModel = new CncInspectionModulePipelineViewModel(
editorViewModel,
imageProcessingService,
persistenceService,
logger);
InspectionModulePipelineEditor.DataContext = _inspectionModulePipelineViewModel;
InspectionModulePipelineEmptyState.DataContext = _inspectionModulePipelineViewModel;
}
catch (Exception)
{
// keep page usable even if pipeline editor host setup fails
}
}
private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (DataContext is CncEditorViewModel viewModel)
{
viewModel.SelectedNode = e.NewValue as CncNodeViewModel;
}
} }
} }
}
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var invert = string.Equals(parameter as string, "Invert", StringComparison.OrdinalIgnoreCase);
var isVisible = value != null;
if (invert)
{
isVisible = !isVisible;
}
return isVisible ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
@@ -11,25 +11,23 @@
<UserControl.Resources> <UserControl.Resources>
<SolidColorBrush x:Key="PanelBg" Color="White" /> <SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" /> <SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" /> <SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" /> <SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily> <FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<!-- 节点项样式 -->
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem"> <Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" /> <Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style> </Style>
<!-- 工具栏按钮样式 -->
<Style x:Key="ToolbarBtn" TargetType="Button"> <Style x:Key="ToolbarBtn" TargetType="Button">
<Setter Property="Width" Value="28" /> <Setter Property="Width" Value="52" />
<Setter Property="Height" Value="28" /> <Setter Property="Height" Value="28" />
<Setter Property="Margin" Value="2,0" /> <Setter Property="Margin" Value="2,0" />
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#cdcbcb" /> <Setter Property="BorderBrush" Value="#CDCBCB" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="FontFamily" Value="Microsoft YaHei UI" /> <Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
@@ -44,21 +42,13 @@
CornerRadius="4"> CornerRadius="4">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<!-- Row 0: 工具栏 -->
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<!-- Row 2: 流水线节点列表 -->
<RowDefinition Height="2*" MinHeight="180" /> <RowDefinition Height="2*" MinHeight="180" />
<!-- Row 3: 分隔线 -->
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<!-- Row 4: 参数面板 -->
<RowDefinition Height="2*" MinHeight="80" /> <RowDefinition Height="2*" MinHeight="80" />
<!-- Row 5: 状态栏 -->
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- 标题栏:流水线名称 + 设备选择 -->
<!-- 工具栏 -->
<Border <Border
Grid.Row="0" Grid.Row="0"
Padding="6,4" Padding="6,4"
@@ -76,50 +66,23 @@
Command="{Binding SavePipelineCommand}" Command="{Binding SavePipelineCommand}"
Content="保存" Content="保存"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="保存流水线" /> ToolTip="保存当前检测模块流水线" />
<Button <Button
Width="43" Width="60"
Command="{Binding SaveAsPipelineCommand}" Command="{Binding SaveAsPipelineCommand}"
Content="另存为" Content="另存为"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="另存为" /> ToolTip="导出当前检测模块流水线" />
<Button <Button
Width="43" Width="52"
Command="{Binding LoadPipelineCommand}" Command="{Binding LoadPipelineCommand}"
Content="加载" Content="加载"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="加载流水线" /> ToolTip="流水线模板加载到当前检测模块" />
<!--
<Button
Width="64"
Command="{Binding LoadImageCommand}"
Content="加载图像"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载输入图像" />
-->
<Button
Command="{Binding ExecutePipelineCommand}"
Content="▶"
Style="{StaticResource ToolbarBtn}"
ToolTip="执行流水线" />
<Button
Command="{Binding CancelExecutionCommand}"
Content="■"
Style="{StaticResource ToolbarBtn}"
ToolTip="取消执行" />
<Button
Command="{Binding DeletePipelineCommand}"
Content="🗑"
Style="{StaticResource ToolbarBtn}"
ToolTip="工具箱" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
<!-- 流水线节点列表(拖拽目标) -->
<ListBox <ListBox
x:Name="PipelineListBox" x:Name="PipelineListBox"
Grid.Row="1" Grid.Row="1"
@@ -138,7 +101,6 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 连接线列:上半段 + 下半段 -->
<Line <Line
x:Name="TopLine" x:Name="TopLine"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@@ -159,7 +121,6 @@
Y1="0" Y1="0"
Y2="14" /> Y2="14" />
<!-- 算子图标 -->
<Border <Border
Grid.Column="0" Grid.Column="0"
Width="28" Width="28"
@@ -177,7 +138,6 @@
Text="{Binding IconPath}" /> Text="{Binding IconPath}" />
</Border> </Border>
<!-- 算子名称 -->
<TextBlock <TextBlock
Grid.Column="1" Grid.Column="1"
Margin="6,0,0,0" Margin="6,0,0,0"
@@ -186,7 +146,6 @@
FontSize="12" FontSize="12"
Text="{Binding DisplayName}" /> Text="{Binding DisplayName}" />
<!-- 操作按钮:上移 / 下移 / 删除(悬停显示) -->
<StackPanel <StackPanel
x:Name="NodeActions" x:Name="NodeActions"
Grid.Column="2" Grid.Column="2"
@@ -199,11 +158,11 @@
Height="22" Height="22"
Margin="1,0" Margin="1,0"
Background="Transparent" Background="Transparent"
BorderBrush="#cdcbcb" BorderBrush="#CDCBCB"
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="" Content=""
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="上移" /> ToolTip="上移" />
@@ -212,11 +171,11 @@
Height="22" Height="22"
Margin="1,0" Margin="1,0"
Background="Transparent" Background="Transparent"
BorderBrush="#cdcbcb" BorderBrush="#CDCBCB"
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="" Content=""
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="下移" /> ToolTip="下移" />
@@ -229,7 +188,7 @@
BorderThickness="1" BorderThickness="1"
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="" Content=""
Cursor="Hand" Cursor="Hand"
FontSize="10" FontSize="10"
ToolTip="删除" /> ToolTip="删除" />
@@ -246,13 +205,12 @@
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<!-- 分隔线 -->
<Rectangle <Rectangle
Grid.Row="2" Grid.Row="2"
Height="1" Height="1"
Fill="{StaticResource SeparatorBrush}" /> Fill="{StaticResource SeparatorBrush}" />
<!-- 参数面板 -->
<ScrollViewer <ScrollViewer
Grid.Row="3" Grid.Row="3"
HorizontalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled"
@@ -283,14 +241,14 @@
<TextBox <TextBox
Grid.Column="1" Grid.Column="1"
Padding="4,2" Padding="4,2"
BorderBrush="#cdcbcb" BorderBrush="#CDCBCB"
BorderThickness="1" BorderThickness="1"
FontFamily="Microsoft YaHei UI" FontFamily="Microsoft YaHei UI"
FontSize="11" FontSize="11"
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"> Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style> <TextBox.Style>
<Style TargetType="TextBox"> <Style TargetType="TextBox">
<Setter Property="BorderBrush" Value="#cdcbcb" /> <Setter Property="BorderBrush" Value="#CDCBCB" />
<Setter Property="Background" Value="White" /> <Setter Property="Background" Value="White" />
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding IsValueValid}" Value="False"> <DataTrigger Binding="{Binding IsValueValid}" Value="False">
@@ -308,7 +266,6 @@
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
<!-- 状态栏 -->
<Border <Border
Grid.Row="4" Grid.Row="4"
Padding="6,4" Padding="6,4"
@@ -1,5 +1,5 @@
using System;
using Prism.Ioc; using Prism.Ioc;
using System;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
@@ -23,7 +23,7 @@ namespace XplorePlane.Views
private void OnLoaded(object sender, RoutedEventArgs e) private void OnLoaded(object sender, RoutedEventArgs e)
{ {
if (DataContext is not PipelineEditorViewModel) if (DataContext is not IPipelineEditorHostViewModel)
{ {
try try
{ {
@@ -46,36 +46,35 @@ namespace XplorePlane.Views
private void OnDragOver(object sender, DragEventArgs e) private void OnDragOver(object sender, DragEventArgs e)
{ {
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat)) e.Effects = e.Data.GetDataPresent(OperatorToolboxView.DragFormat)
e.Effects = DragDropEffects.Copy; ? DragDropEffects.Copy
else : DragDropEffects.None;
e.Effects = DragDropEffects.None;
e.Handled = true; e.Handled = true;
} }
private void OnOperatorDropped(object sender, DragEventArgs e) private void OnOperatorDropped(object sender, DragEventArgs e)
{ {
if (DataContext is not PipelineEditorViewModel vm) if (DataContext is not IPipelineEditorHostViewModel vm)
{ {
_logger?.Warn("Drop 事件触发但 DataContext 不是 PipelineEditorViewModel"); _logger?.Warn("Drop 事件触发但 DataContext 不是流水线宿主 ViewModel");
return; return;
} }
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat)) if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
{ {
_logger?.Warn("Drop 事件触发但数据中没有 {Format}", OperatorToolboxView.DragFormat); _logger?.Warn("Drop 事件触发但数据中没有 {Format}", OperatorToolboxView.DragFormat);
return; return;
} }
var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string; var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string;
if (string.IsNullOrEmpty(operatorKey)) if (string.IsNullOrWhiteSpace(operatorKey))
{ {
_logger?.Warn("Drop 事件触发但 OperatorKey 为空"); _logger?.Warn("Drop 事件触发但 OperatorKey 为空");
return; return;
} }
_logger?.Info("算子已放入流水线:{OperatorKey}VM HashCode={Hash}当前节点数(执行前)={Count}", _logger?.Info("算子已放入流水线:{OperatorKey},当前节点数(执行前)={Count}",
operatorKey, vm.GetHashCode(), vm.PipelineNodes.Count); operatorKey, vm.PipelineNodes.Count);
vm.AddOperatorCommand.Execute(operatorKey); vm.AddOperatorCommand.Execute(operatorKey);
_logger?.Info("AddOperator 执行后节点数={Count}PipelineListBox.Items.Count={ItemsCount}", _logger?.Info("AddOperator 执行后节点数={Count}PipelineListBox.Items.Count={ItemsCount}",
vm.PipelineNodes.Count, PipelineListBox.Items.Count); vm.PipelineNodes.Count, PipelineListBox.Items.Count);
+3 -4
View File
@@ -1,10 +1,9 @@
<UserControl <UserControl
x:Class="XplorePlane.Views.ImagePanelView" x:Class="XplorePlane.Views.ImagePanelView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:XplorePlane.Views"
d:DesignHeight="400" d:DesignHeight="400"
d:DesignWidth="250" d:DesignWidth="250"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -14,9 +13,9 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1"> <Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
<TextBlock Margin="8,4" HorizontalAlignment="Left" VerticalAlignment="Center" <TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
FontWeight="SemiBold" Foreground="#333333" Text="图像" /> FontWeight="SemiBold" Foreground="#333333" Text="图像" />
</Border> </Border>
<views:PipelineEditorView Grid.Row="1" /> <ContentControl Grid.Row="1" Content="{Binding ImagePanelContent}" />
</Grid> </Grid>
</UserControl> </UserControl>
+29 -17
View File
@@ -16,7 +16,7 @@
Height="1040" Height="1040"
d:DesignWidth="1580" d:DesignWidth="1580"
Background="#F5F5F5" Background="#F5F5F5"
Icon="pack://application:,,,/XplorerPlane.ico" Icon="pack://application:,,,/XplorePlane;component/XplorerPlane.ico"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.Resources> <Window.Resources>
@@ -74,7 +74,7 @@
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="新建CNC" telerik:ScreenTip.Title="新建CNC"
Command="{Binding Path=SetStyle.Command}" Command="{Binding NewCncProgramCommand}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/new-doc.png" SmallImage="/Assets/Icons/new-doc.png"
Text="新建CNC" /> Text="新建CNC" />
@@ -82,11 +82,12 @@
telerik:ScreenTip.Description="保存当前X射线实时图像" telerik:ScreenTip.Description="保存当前X射线实时图像"
telerik:ScreenTip.Title="保存图像" telerik:ScreenTip.Title="保存图像"
Size="Medium" Size="Medium"
Command="{Binding SaveCncProgramCommand}"
SmallImage="/Assets/Icons/save.png" SmallImage="/Assets/Icons/save.png"
Text="保存" /> Text="保存" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="另存为" telerik:ScreenTip.Title="另存为"
Command="{Binding OpenFileCommand}" Command="{Binding SaveCncProgramCommand}"
Size="Medium" Size="Medium"
SmallImage="/Assets/Icons/saveas.png" SmallImage="/Assets/Icons/saveas.png"
Text="另存为" /> Text="另存为" />
@@ -94,7 +95,7 @@
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="加载CNC" telerik:ScreenTip.Title="加载CNC"
Command="{Binding OpenFileCommand}" Command="{Binding LoadCncProgramCommand}"
Size="Large" Size="Large"
SmallImage="/Assets/Icons/open.png" SmallImage="/Assets/Icons/open.png"
Text="加载CNC" /> Text="加载CNC" />
@@ -284,32 +285,28 @@
Command="{Binding OpenCncEditorCommand}" Command="{Binding OpenCncEditorCommand}"
Size="Large" Size="Large"
SmallImage="/Assets/Icons/cnc.png" SmallImage="/Assets/Icons/cnc.png"
Text="CNC 编辑" /> Text="CNC 编辑" />
<!-- 矩阵编排入口按钮 -->
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
telerik:ScreenTip.Title="矩阵编排"
Command="{Binding OpenMatrixEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/matrix.png"
Text="矩阵编排" />
<!-- CNC 节点快捷工具 --> <!-- CNC 节点快捷工具 -->
<StackPanel> <StackPanel>
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="参考点" telerik:ScreenTip.Title="参考点"
Size="Medium" Size="Medium"
Command="{Binding InsertReferencePointCommand}"
SmallImage="/Assets/Icons/reference.png" SmallImage="/Assets/Icons/reference.png"
Text="参考点" /> Text="参考点" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="添加位置" telerik:ScreenTip.Title="添加位置"
Size="Medium" Size="Medium"
Command="{Binding InsertSavePositionCommand}"
SmallImage="/Assets/Icons/add-pos.png" SmallImage="/Assets/Icons/add-pos.png"
Text="添加位置" /> Text="添加位置" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="完成" telerik:ScreenTip.Title="完成"
Size="Medium" Size="Medium"
Command="{Binding InsertCompleteProgramCommand}"
SmallImage="/Assets/Icons/finish.png" SmallImage="/Assets/Icons/finish.png"
Text="完成" /> Text="完成" />
</StackPanel> </StackPanel>
@@ -317,16 +314,19 @@
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="标记" telerik:ScreenTip.Title="标记"
Size="Medium" Size="Medium"
Command="{Binding InsertInspectionMarkerCommand}"
SmallImage="/Assets/Icons/mark.png" SmallImage="/Assets/Icons/mark.png"
Text="标记" /> Text="标记" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="模块" telerik:ScreenTip.Title="模块"
Size="Medium" Size="Medium"
Command="{Binding InsertInspectionModuleCommand}"
SmallImage="/Assets/Icons/Module.png" SmallImage="/Assets/Icons/Module.png"
Text="检测模块" /> Text="检测模块" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="全部保存" telerik:ScreenTip.Title="全部保存"
Size="Medium" Size="Medium"
Command="{Binding SaveCncProgramCommand}"
SmallImage="/Assets/Icons/saveall.png" SmallImage="/Assets/Icons/saveall.png"
Text="全部保存" /> Text="全部保存" />
</StackPanel> </StackPanel>
@@ -334,14 +334,26 @@
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="消息" telerik:ScreenTip.Title="消息"
Size="Medium" Size="Medium"
Command="{Binding InsertPauseDialogCommand}"
SmallImage="/Assets/Icons/message.png" SmallImage="/Assets/Icons/message.png"
Text="消息弹窗" /> Text="消息弹窗" />
<telerik:RadRibbonButton <telerik:RadRibbonButton
telerik:ScreenTip.Title="等待" telerik:ScreenTip.Title="等待"
Size="Medium" Size="Medium"
Command="{Binding InsertWaitDelayCommand}"
SmallImage="/Assets/Icons/wait.png" SmallImage="/Assets/Icons/wait.png"
Text="插入等待" /> Text="插入等待" />
</StackPanel> </StackPanel>
<!-- 矩阵编排入口按钮 -->
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开矩阵编排窗口,配置多工件阵列检测方案"
telerik:ScreenTip.Title="矩阵编排"
Command="{Binding OpenMatrixEditorCommand}"
Size="Large"
SmallImage="/Assets/Icons/matrix.png"
Text="矩阵编排" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}"> <telerik:RadRibbonGroup Header="高级模块" IsEnabled="{Binding Path=CellsGroup.IsEnabled}">
@@ -388,7 +400,7 @@
Size="Large" Size="Large"
SmallImage="/Assets/Icons/spiral.png" /> SmallImage="/Assets/Icons/spiral.png" />
</telerik:RadRibbonGroup> </telerik:RadRibbonGroup>
<!-- <!--
<telerik:RadRibbonGroup Header="图像处理"> <telerik:RadRibbonGroup Header="图像处理">
<telerik:RadRibbonGroup.Variants> <telerik:RadRibbonGroup.Variants>
<telerik:GroupVariant Priority="0" Variant="Large" /> <telerik:GroupVariant Priority="0" Variant="Large" />
@@ -458,9 +470,9 @@
Margin="0"> Margin="0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition x:Name="NavColumn" Width="0" /> <ColumnDefinition x:Name="NavColumn" Width="0" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="{Binding ViewportPanelWidth}" />
<ColumnDefinition Width="350" /> <ColumnDefinition Width="{Binding ImagePanelWidth}" />
<ColumnDefinition Width="350" /> <ColumnDefinition Width="300" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 左侧: 计划导航 (默认隐藏,点击CNC AccountingNumberFormatButton显示) --> <!-- 左侧: 计划导航 (默认隐藏,点击CNC AccountingNumberFormatButton显示) -->
@@ -19,7 +19,7 @@
<!-- 标题栏 --> <!-- 标题栏 -->
<Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1"> <Border Grid.Row="0" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1">
<TextBlock Margin="8,4" HorizontalAlignment="Left" VerticalAlignment="Center" <TextBlock Margin="4,2" HorizontalAlignment="Left" VerticalAlignment="Center"
FontWeight="SemiBold" Foreground="#333333" Text="实时图像" /> FontWeight="SemiBold" Foreground="#333333" Text="实时图像" />
</Border> </Border>
@@ -30,7 +30,7 @@
<!-- 图像信息栏 --> <!-- 图像信息栏 -->
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0"> <Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
<TextBlock Margin="8,2" FontSize="11" Foreground="#666666" <TextBlock Margin="4,2" FontSize="11" Foreground="#666666"
Text="{Binding ImageInfo}" /> Text="{Binding ImageInfo}" />
</Border> </Border>
</Grid> </Grid>
+3
View File
@@ -146,6 +146,9 @@
<Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link> <Link>Libs\Hardware\zh-TW\%(Filename)%(Extension)</Link>
</None> </None>
<Compile Remove="Views\Main\MainWindowB.xaml.cs" /> <Compile Remove="Views\Main\MainWindowB.xaml.cs" />
<Content Include="XplorerPlane.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Resource Include="XplorerPlane.ico" /> <Resource Include="XplorerPlane.ico" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 92 KiB