将自定义类别配置文件整合到系统配置文件中;修复配置事件重复触发问题。

This commit is contained in:
tian 2026-03-09 14:04:25 +08:00
parent dc29c3526e
commit 07b30a4fb5
11 changed files with 453 additions and 612 deletions

View File

@ -483,11 +483,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\default_config.toml</Link>
</None>
<!-- Custom Categories Configuration File -->
<None Include="resources\default_custom_categories.toml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>resources\default_custom_categories.toml</Link>
</None>
<!-- Plugin Name File (in resources folder) -->
<None Include="resources\TransportPlugin.name.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

View File

@ -71,3 +71,13 @@ width_limit_meters = 3.0
# ZUp: 强制使用 Z-Up 坐标系Navisworks 默认)
# YUp: 强制使用 Y-Up 坐标系(常见于 Revit 导出)
type = "AutoDetect"
# 自定义物流类别
# 用于扩展内置类别,只需填写类别名称
#
# 示例:
# [[custom_category]]
# name = "狭窄通道"
#
# [[custom_category]]
# name = "临时存储区"

View File

@ -1,86 +0,0 @@
# 自定义物流类别配置文件
# 位置: %ProgramData%/Autodesk/Navisworks Manage 2026/plugins/TransportPlugin/custom_categories.toml
# 说明: 本文件定义用户自定义的物流类别,与模型文件无关,可跨项目共享
# ============================================================
# 类别属性说明
# ============================================================
#
# 【必填属性】
# id: 唯一标识(英文,用于存储和内部识别)
# - 不能与内置类别名称重复(如 door, elevator 等)
# - 建议使用小写和下划线,如: "ramp", "clean_room"
#
# display_name: 显示名称(中文)
# - 在UI类别下拉框和物流模型列表中显示
#
# 【可选属性 - 有特殊需求时才设置】
# traversable: 是否默认可通行
# - 默认: true可通行
# - 仅当类别不可通行时设置为 false如限制区域
#
# weight: 寻路权重
# - 默认: 1.0
# - 值越小优先级越高,越大越难走
# - 不可通行区域建议设为极大值如 999999.0
#
# 【预留属性 - 当前未使用,可不设置】
# icon: 图标标识(默认 Box
# priority: 优先级 1-10默认 5
# color: UI显示颜色默认 #9E9E9E
# height_limit_meters: 默认高度限制米数(默认 3.0
# speed_limit_meters_per_second: 默认速度限制米/秒(默认 1.0
# width_limit_meters: 默认宽度限制米数(默认 3.0
#
# ============================================================
# 配置示例
# ============================================================
# --------------------------------------------------
# 示例1: 最简配置(只有必填,其他用默认)
# --------------------------------------------------
# 坡道:默认可通行(weight=1.0),无需设置其他属性
[[category]]
id = "ramp"
display_name = "坡道"
# --------------------------------------------------
# 示例2: 不可通行区域(必须设置特殊属性)
# --------------------------------------------------
# 限制区域:不可通行,寻路时避开
[[category]]
id = "restricted_area"
display_name = "限制区域"
traversable = false
weight = 999999.0
# --------------------------------------------------
# 示例3: 优先选择的通道设置更小weight
# --------------------------------------------------
# 快速通道:比默认值(1.0)优先,寻路时优先走这里
[[category]]
id = "express_lane"
display_name = "快速通道"
weight = 0.6
# --------------------------------------------------
# 示例4: 较难的通行区域设置更大weight
# --------------------------------------------------
# 狭窄通道:比默认值难走,只有必要时才走
[[category]]
id = "narrow_passage"
display_name = "狭窄通道"
weight = 2.5
# --------------------------------------------------
# 示例5: 完整配置(展示所有可用属性)
# --------------------------------------------------
# 一般不需要这么多,按需设置即可
[[category]]
id = "temporary_storage"
display_name = "临时堆放区"
# 可通行但较慢weight > 1.0
weight = 1.5
# 预留属性(当前未使用)
icon = "Package"
color = "#FFC107"

View File

@ -488,6 +488,28 @@ namespace NavisworksTransport.Core.Config
}
}
// 加载自定义类别配置(可选)
if (model.ContainsKey("custom_category"))
{
var categoriesArray = model["custom_category"] as TomlTableArray;
if (categoriesArray != null)
{
foreach (var categoryTable in categoriesArray)
{
var name = GetStringValueWithDefault(categoryTable, "name", null, missingItems);
if (!string.IsNullOrEmpty(name))
{
config.CustomCategories.Add(new CustomCategoryConfigItem
{
Name = name
});
}
}
LogManager.Info($"[ConfigManager] 已加载 {config.CustomCategories.Count} 个自定义类别");
}
}
// 如果有缺失项,记录警告并设置标志
if (missingItems.Count > 0)
{
@ -513,7 +535,8 @@ namespace NavisworksTransport.Core.Config
Visualization = new VisualizationConfig(),
Animation = new AnimationConfig(),
Logistics = new LogisticsConfig(),
CoordinateSystem = new CoordinateSystemConfig()
CoordinateSystem = new CoordinateSystemConfig(),
CustomCategories = new List<CustomCategoryConfigItem>()
};
}
@ -597,7 +620,24 @@ namespace NavisworksTransport.Core.Config
ApplyConfigValuesToModel(model, config);
// 转换回 TOML 字符串
return Toml.FromModel(model);
var tomlContent = Toml.FromModel(model);
// 追加自定义类别配置
if (config.CustomCategories != null && config.CustomCategories.Count > 0)
{
var sb = new System.Text.StringBuilder(tomlContent);
sb.AppendLine();
sb.AppendLine("# 自定义物流类别(只需填写名称)");
foreach (var category in config.CustomCategories)
{
sb.AppendLine("[[custom_category]]");
sb.AppendLine($"name = \"{EscapeTomlString(category.Name)}\"");
sb.AppendLine();
}
tomlContent = sb.ToString();
}
return tomlContent;
}
/// <summary>
@ -669,6 +709,10 @@ namespace NavisworksTransport.Core.Config
coordSys["type"] = config.CoordinateSystem.Type;
}
}
// 更新自定义类别配置
// 注意由于Tomlyn对数组的处理限制自定义类别通过专门的API管理
// 这里不直接操作TOML模型而是在保存前通过模板合并
}
/// <summary>
@ -679,6 +723,52 @@ namespace NavisworksTransport.Core.Config
ReloadInternal(isExternal: false);
}
/// <summary>
/// 从内容重新加载配置(用于编辑器保存后不触发文件监控)
/// </summary>
public void ReloadFromContent(string tomlContent, bool notifyChange = true)
{
try
{
// 先临时禁用文件监控,避免写入时触发外部修改事件
var watcherWasEnabled = _fileWatcher?.EnableRaisingEvents ?? false;
if (_fileWatcher != null)
{
_fileWatcher.EnableRaisingEvents = false;
}
try
{
// 写入文件(此时 FileSystemWatcher 已禁用,不会触发外部修改事件)
File.WriteAllText(ConfigFilePath, tomlContent);
// 解析并更新配置
_currentConfig = ParseTomlToConfig(tomlContent);
LogManager.Info("配置已从内容重新加载");
// 触发配置变更事件
if (notifyChange)
{
NotifyConfigurationChanged(ConfigCategory.All, isExternal: false);
}
}
finally
{
// 恢复文件监控
if (_fileWatcher != null && watcherWasEnabled)
{
_fileWatcher.EnableRaisingEvents = true;
}
}
}
catch (Exception ex)
{
LogManager.Error($"从内容重新加载配置失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 内部重新加载方法
/// </summary>
@ -994,6 +1084,16 @@ namespace NavisworksTransport.Core.Config
return strValue;
}
/// <summary>
/// 转义 TOML 字符串
/// </summary>
private string EscapeTomlString(string str)
{
if (string.IsNullOrEmpty(str))
return "";
return str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
}
#endregion
}
}

View File

@ -1,47 +1,16 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using Tomlyn;
using Tomlyn.Model;
namespace NavisworksTransport.Core.Config
{
/// <summary>
/// 自定义物流类别配置
/// 从用户级 TOML 配置文件加载,独立于模型数据库
/// 自定义物流类别配置管理器
/// 从 ConfigManager 获取配置,不再使用独立的配置文件
/// </summary>
public class CustomCategoryConfig
{
/// <summary>
/// 配置文件路径(用户级,与模型无关)
/// </summary>
public static string ConfigFilePath
{
get
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"Autodesk",
"Navisworks Manage 2026",
"plugins",
"TransportPlugin",
"custom_categories.toml"
);
}
}
/// <summary>
/// 配置目录
/// </summary>
public static string ConfigDirectory => Path.GetDirectoryName(ConfigFilePath);
/// <summary>
/// 自定义类别列表
/// </summary>
public List<CustomCategoryDefinition> Categories { get; set; } = new List<CustomCategoryDefinition>();
/// <summary>
/// 获取所有类别(包括内置和自定义)
/// </summary>
@ -51,27 +20,27 @@ namespace NavisworksTransport.Core.Config
foreach (var builtIn in BuiltInCategories.All)
yield return builtIn;
// 自定义类别
foreach (var custom in Categories)
// 自定义类别(从 ConfigManager 获取)
foreach (var custom in GetCustomCategories())
yield return custom;
}
/// <summary>
/// 根据ID获取类别定义
/// 根据名称获取类别定义
/// </summary>
public CategoryDefinition GetCategory(string id)
public CategoryDefinition GetCategory(string name)
{
if (string.IsNullOrEmpty(id))
if (string.IsNullOrEmpty(name))
return null;
// 先查内置
var builtIn = BuiltInCategories.GetById(id);
var builtIn = BuiltInCategories.GetById(name);
if (builtIn != null)
return builtIn;
// 再查自定义
return Categories.FirstOrDefault(c =>
c.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
// 再查自定义(按名称匹配)
return GetCustomCategories().FirstOrDefault(c =>
c.DisplayName.Equals(name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
@ -91,310 +60,88 @@ namespace NavisworksTransport.Core.Config
}
/// <summary>
/// 从 TOML 文件加载配置
/// 添加自定义类别
/// </summary>
public static CustomCategoryConfig LoadFromFile()
public void AddCategory(string name)
{
var config = new CustomCategoryConfig();
if (string.IsNullOrEmpty(name))
throw new ArgumentException("类别名称不能为空");
try
if (CategoryExists(name))
throw new InvalidOperationException($"类别 '{name}' 已存在");
var config = ConfigManager.Instance.Current;
config.CustomCategories.Add(new CustomCategoryConfigItem
{
if (!File.Exists(ConfigFilePath))
{
// 配置文件不存在,创建默认配置
config = CreateDefaultConfig();
config.SaveToFile();
return config;
}
var tomlContent = File.ReadAllText(ConfigFilePath);
var tomlTable = Toml.ToModel(tomlContent);
if (tomlTable.TryGetValue("category", out var categoriesObj) &&
categoriesObj is TomlTableArray categoriesArray)
{
foreach (var categoryTable in categoriesArray)
{
var category = ParseCategoryFromToml(categoryTable);
if (category != null)
config.Categories.Add(category);
}
}
LogManager.Info($"[CustomCategoryConfig] 已加载 {config.Categories.Count} 个自定义类别");
}
catch (Exception ex)
{
LogManager.Error($"[CustomCategoryConfig] 加载配置失败: {ex.Message}");
// 加载失败时返回空配置
config = new CustomCategoryConfig();
}
return config;
Name = name
});
ConfigManager.Instance.SaveConfig(config);
LogManager.Info($"[CustomCategoryConfig] 已添加自定义类别: {name}");
}
/// <summary>
/// 保存配置到 TOML 文件
/// 删除自定义类别
/// </summary>
public void SaveToFile()
public void RemoveCategory(string name)
{
try
if (IsBuiltIn(name))
throw new InvalidOperationException("不能删除内置类别");
var config = ConfigManager.Instance.Current;
var category = config.CustomCategories.FirstOrDefault(c =>
c.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (category != null)
{
Directory.CreateDirectory(ConfigDirectory);
var tomlContent = GenerateTomlContent();
File.WriteAllText(ConfigFilePath, tomlContent);
LogManager.Info($"[CustomCategoryConfig] 配置已保存: {ConfigFilePath}");
}
catch (Exception ex)
{
LogManager.Error($"[CustomCategoryConfig] 保存配置失败: {ex.Message}");
config.CustomCategories.Remove(category);
ConfigManager.Instance.SaveConfig(config);
LogManager.Info($"[CustomCategoryConfig] 已删除自定义类别: {name}");
}
}
/// <summary>
/// 默认配置文件路径(插件资源目录)
/// 从 ConfigManager 获取自定义类别列表
/// </summary>
public static string DefaultConfigFilePath
private IEnumerable<CustomCategoryDefinition> GetCustomCategories()
{
get
{
return Path.Combine(
Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location),
"resources",
"default_custom_categories.toml"
);
}
}
var config = ConfigManager.Instance.Current;
if (config.CustomCategories == null)
yield break;
/// <summary>
/// 创建默认配置
/// 尝试从默认配置文件加载示例,如果失败则返回空配置
/// </summary>
private static CustomCategoryConfig CreateDefaultConfig()
{
var config = new CustomCategoryConfig
foreach (var item in config.CustomCategories)
{
Categories = new List<CustomCategoryDefinition>()
};
try
{
// 尝试从默认配置文件加载示例
if (File.Exists(DefaultConfigFilePath))
yield return new CustomCategoryDefinition
{
var tomlContent = File.ReadAllText(DefaultConfigFilePath);
var tomlTable = Toml.ToModel(tomlContent);
if (tomlTable.TryGetValue("category", out var categoriesObj) &&
categoriesObj is TomlTableArray categoriesArray)
{
foreach (var categoryTable in categoriesArray)
{
var category = ParseCategoryFromToml(categoryTable);
if (category != null)
config.Categories.Add(category);
}
}
LogManager.Info($"[CustomCategoryConfig] 已从默认配置文件加载 {config.Categories.Count} 个示例类别");
}
}
catch (Exception ex)
{
LogManager.Warning($"[CustomCategoryConfig] 从默认配置文件加载失败: {ex.Message},将使用空配置");
}
return config;
}
/// <summary>
/// 从 TOML 表解析类别定义
/// </summary>
private static CustomCategoryDefinition ParseCategoryFromToml(TomlTable table)
{
try
{
var category = new CustomCategoryDefinition
{
Id = table.GetString("id"),
DisplayName = table.GetString("display_name"),
Icon = table.GetString("icon", "Box"),
Traversable = table.GetBool("traversable", true),
Priority = table.GetInt("priority", 5),
Weight = table.GetDouble("weight", 1.0),
DisplayColor = ParseColor(table.GetString("color", "#9E9E9E")),
Defaults = new CategoryDefaultProperties
{
HeightLimitMeters = table.GetDouble("height_limit_meters", 3.0),
SpeedLimitMetersPerSecond = table.GetDouble("speed_limit_meters_per_second", 1.0),
WidthLimitMeters = table.GetDouble("width_limit_meters", 3.0)
}
Id = item.Name, // 名称同时作为ID
DisplayName = item.Name
};
// 验证必填字段
if (string.IsNullOrEmpty(category.Id) || string.IsNullOrEmpty(category.DisplayName))
{
LogManager.Warning("[CustomCategoryConfig] 类别定义缺少必填字段 id 或 display_name");
return null;
}
// 检查ID是否与内置类别冲突
if (BuiltInCategories.GetById(category.Id) != null)
{
LogManager.Warning($"[CustomCategoryConfig] 自定义类别ID '{category.Id}' 与内置类别冲突,已跳过");
return null;
}
return category;
}
catch (Exception ex)
{
LogManager.Error($"[CustomCategoryConfig] 解析类别定义失败: {ex.Message}");
return null;
}
}
/// <summary>
/// 生成 TOML 内容(只输出非默认值的属性)
/// </summary>
private string GenerateTomlContent()
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("# 自定义物流类别配置文件");
sb.AppendLine("# 位置: %ProgramData%/Autodesk/Navisworks Manage 2026/plugins/TransportPlugin/custom_categories.toml");
sb.AppendLine("# 说明: 本文件定义用户自定义的物流类别,与模型文件无关,可跨项目共享");
sb.AppendLine();
sb.AppendLine("# ============================================================");
sb.AppendLine("# 类别属性说明");
sb.AppendLine("# ============================================================");
sb.AppendLine("#");
sb.AppendLine("# 【必填】");
sb.AppendLine("# id: 唯一标识(英文)");
sb.AppendLine("# display_name: 显示名称(中文)");
sb.AppendLine("#");
sb.AppendLine("# 【可选 - 有特殊需求才设置】");
sb.AppendLine("# traversable: 是否可通行(默认 true");
sb.AppendLine("# weight: 寻路权重(默认 1.0");
sb.AppendLine("#");
sb.AppendLine("# 【预留 - 当前未使用】");
sb.AppendLine("# icon, priority, color, height_limit_meters 等");
sb.AppendLine("#");
sb.AppendLine();
foreach (var category in Categories)
{
sb.AppendLine("[[category]]");
sb.AppendLine($"id = \"{EscapeTomlString(category.Id)}\"");
sb.AppendLine($"display_name = \"{EscapeTomlString(category.DisplayName)}\"");
// 只输出非默认值的属性
if (category.Traversable != true)
sb.AppendLine($"traversable = {category.Traversable.ToString().ToLower()}");
if (category.Weight != 1.0)
sb.AppendLine($"weight = {category.Weight:F2}");
sb.AppendLine();
}
return sb.ToString();
}
/// <summary>
/// 解析颜色字符串
/// </summary>
private static Color ParseColor(string colorStr)
{
try
{
if (string.IsNullOrEmpty(colorStr))
return Color.Gray;
// 支持 #RRGGBB 或 #AARRGGBB 格式
if (colorStr.StartsWith("#"))
colorStr = colorStr.Substring(1);
if (colorStr.Length == 6)
{
int r = int.Parse(colorStr.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
int g = int.Parse(colorStr.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
int b = int.Parse(colorStr.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
return Color.FromArgb(r, g, b);
}
else if (colorStr.Length == 8)
{
int a = int.Parse(colorStr.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
int r = int.Parse(colorStr.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
int g = int.Parse(colorStr.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
int b = int.Parse(colorStr.Substring(6, 2), System.Globalization.NumberStyles.HexNumber);
return Color.FromArgb(a, r, g, b);
}
}
catch { }
return Color.Gray;
}
/// <summary>
/// 颜色转十六进制字符串
/// </summary>
private static string ColorToHex(Color color)
{
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
}
/// <summary>
/// 转义 TOML 字符串
/// </summary>
private static string EscapeTomlString(string str)
{
if (string.IsNullOrEmpty(str))
return "";
return str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
}
}
/// <summary>
/// 类别定义基类(内置和自定义共用接口)
/// 简化版,仅保留核心属性
/// </summary>
public abstract class CategoryDefinition
{
public abstract string Id { get; set; }
public abstract string DisplayName { get; set; }
public abstract string Icon { get; set; }
public abstract bool Traversable { get; set; }
public abstract int Priority { get; set; }
public abstract double Weight { get; set; }
public abstract Color DisplayColor { get; set; }
public abstract CategoryDefaultProperties Defaults { get; set; }
}
/// <summary>
/// 自定义类别定义
/// 自定义类别定义(简化版)
/// </summary>
public class CustomCategoryDefinition : CategoryDefinition
{
public override string Id { get; set; }
public override string DisplayName { get; set; }
public override string Icon { get; set; } = "Box";
public override bool Traversable { get; set; } = true;
public override int Priority { get; set; } = 5;
public override double Weight { get; set; } = 1.0;
public override Color DisplayColor { get; set; } = Color.Gray;
public override CategoryDefaultProperties Defaults { get; set; } = new CategoryDefaultProperties();
}
/// <summary>
/// 类别默认属性
/// </summary>
public class CategoryDefaultProperties
{
public double HeightLimitMeters { get; set; } = 3.0;
public double SpeedLimitMetersPerSecond { get; set; } = 1.0;
public double WidthLimitMeters { get; set; } = 3.0;
}
/// <summary>
/// 内置类别定义(包装 LogisticsElementType 枚举)
/// 简化版,仅保留核心属性
/// </summary>
public class BuiltInCategoryDefinition : CategoryDefinition
{
@ -407,134 +154,8 @@ namespace NavisworksTransport.Core.Config
public override string Id { get => _elementType.ToString(); set => throw new NotSupportedException("内置类别不支持修改"); }
public override string DisplayName { get => _elementType.ToString(); set => throw new NotSupportedException("内置类别不支持修改"); }
public override string Icon { get => GetIconForType(_elementType); set => throw new NotSupportedException("内置类别不支持修改"); }
public override bool Traversable { get => GetTraversableForType(_elementType); set => throw new NotSupportedException("内置类别不支持修改"); }
public override int Priority { get => GetPriorityForType(_elementType); set => throw new NotSupportedException("内置类别不支持修改"); }
public override double Weight { get => GetWeightForType(_elementType); set => throw new NotSupportedException("内置类别不支持修改"); }
public override Color DisplayColor { get => GetColorForType(_elementType); set => throw new NotSupportedException("内置类别不支持修改"); }
public override CategoryDefaultProperties Defaults { get => GetDefaultsForType(_elementType); set => throw new NotSupportedException("内置类别不支持修改"); }
public CategoryAttributeManager.LogisticsElementType ElementType => _elementType;
private static string GetIconForType(CategoryAttributeManager.LogisticsElementType type)
{
switch (type)
{
case CategoryAttributeManager.LogisticsElementType.: return "BlockHelper";
case CategoryAttributeManager.LogisticsElementType.: return "Rectangle";
case CategoryAttributeManager.LogisticsElementType.: return "Door";
case CategoryAttributeManager.LogisticsElementType.: return "Elevator";
case CategoryAttributeManager.LogisticsElementType.: return "Stairs";
case CategoryAttributeManager.LogisticsElementType.: return "Road";
case CategoryAttributeManager.LogisticsElementType.: return "Corridor";
case CategoryAttributeManager.LogisticsElementType.: return "Truck";
case CategoryAttributeManager.LogisticsElementType.: return "Parking";
case CategoryAttributeManager.LogisticsElementType.: return "Air";
case CategoryAttributeManager.LogisticsElementType.: return "EyeOff";
default: return "Box";
}
}
private static bool GetTraversableForType(CategoryAttributeManager.LogisticsElementType type)
{
switch (type)
{
case CategoryAttributeManager.LogisticsElementType.:
case CategoryAttributeManager.LogisticsElementType.:
case CategoryAttributeManager.LogisticsElementType.:
return false;
default:
return true;
}
}
private static int GetPriorityForType(CategoryAttributeManager.LogisticsElementType type)
{
switch (type)
{
case CategoryAttributeManager.LogisticsElementType.: return 1;
case CategoryAttributeManager.LogisticsElementType.: return 2;
case CategoryAttributeManager.LogisticsElementType.: return 3;
case CategoryAttributeManager.LogisticsElementType.: return 4;
case CategoryAttributeManager.LogisticsElementType.: return 5;
case CategoryAttributeManager.LogisticsElementType.: return 6;
case CategoryAttributeManager.LogisticsElementType.: return 7;
case CategoryAttributeManager.LogisticsElementType.: return 8;
case CategoryAttributeManager.LogisticsElementType.: return 9;
case CategoryAttributeManager.LogisticsElementType.: return 10;
default: return 5;
}
}
private static double GetWeightForType(CategoryAttributeManager.LogisticsElementType type)
{
switch (type)
{
case CategoryAttributeManager.LogisticsElementType.: return 0.5;
case CategoryAttributeManager.LogisticsElementType.: return 0.6;
case CategoryAttributeManager.LogisticsElementType.: return 0.7;
case CategoryAttributeManager.LogisticsElementType.: return 0.8;
case CategoryAttributeManager.LogisticsElementType.: return 0.9;
case CategoryAttributeManager.LogisticsElementType.: return 1.0;
case CategoryAttributeManager.LogisticsElementType.: return 1.2;
case CategoryAttributeManager.LogisticsElementType.: return 2.0;
case CategoryAttributeManager.LogisticsElementType.: return 3.0;
case CategoryAttributeManager.LogisticsElementType.:
case CategoryAttributeManager.LogisticsElementType.:
return double.MaxValue;
default: return 1.0;
}
}
private static Color GetColorForType(CategoryAttributeManager.LogisticsElementType type)
{
switch (type)
{
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(76, 175, 80); // Green
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(129, 199, 132); // Light Green
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(33, 150, 243); // Blue
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(156, 39, 176); // Purple
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(255, 87, 34); // Deep Orange
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(255, 152, 0); // Orange
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(0, 150, 136); // Teal
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(63, 81, 181); // Indigo
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(244, 67, 54); // Red
case CategoryAttributeManager.LogisticsElementType.: return Color.FromArgb(158, 158, 158); // Gray
default: return Color.FromArgb(96, 125, 139); // Blue Gray
}
}
private static CategoryDefaultProperties GetDefaultsForType(CategoryAttributeManager.LogisticsElementType type)
{
var defaults = new CategoryDefaultProperties();
switch (type)
{
case CategoryAttributeManager.LogisticsElementType.:
defaults.HeightLimitMeters = 2.5;
defaults.SpeedLimitMetersPerSecond = 0.5;
defaults.WidthLimitMeters = 2.0;
break;
case CategoryAttributeManager.LogisticsElementType.:
defaults.HeightLimitMeters = 3.0;
defaults.SpeedLimitMetersPerSecond = 0.3;
defaults.WidthLimitMeters = 2.5;
break;
case CategoryAttributeManager.LogisticsElementType.:
defaults.HeightLimitMeters = 2.8;
defaults.SpeedLimitMetersPerSecond = 0.2;
defaults.WidthLimitMeters = 1.5;
break;
case CategoryAttributeManager.LogisticsElementType.:
defaults.HeightLimitMeters = 4.0;
defaults.SpeedLimitMetersPerSecond = 1.5;
defaults.WidthLimitMeters = 4.0;
break;
// 其他使用默认值
}
return defaults;
}
}
/// <summary>
@ -579,44 +200,4 @@ namespace NavisworksTransport.Core.Config
}
}
/// <summary>
/// TOML 扩展方法
/// </summary>
internal static class TomlExtensions
{
public static string GetString(this TomlTable table, string key, string defaultValue = "")
{
if (table.TryGetValue(key, out var value) && value is string str)
return str;
return defaultValue;
}
public static bool GetBool(this TomlTable table, string key, bool defaultValue = false)
{
if (table.TryGetValue(key, out var value) && value is bool b)
return b;
return defaultValue;
}
public static int GetInt(this TomlTable table, string key, int defaultValue = 0)
{
if (table.TryGetValue(key, out var value))
{
if (value is long l) return (int)l;
if (value is int i) return i;
}
return defaultValue;
}
public static double GetDouble(this TomlTable table, string key, double defaultValue = 0.0)
{
if (table.TryGetValue(key, out var value))
{
if (value is double d) return d;
if (value is long l) return l;
if (value is int i) return i;
}
return defaultValue;
}
}
}

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
namespace NavisworksTransport.Core.Config
{
/// <summary>
@ -40,6 +42,22 @@ namespace NavisworksTransport.Core.Config
/// 坐标系配置
/// </summary>
public CoordinateSystemConfig CoordinateSystem { get; set; }
/// <summary>
/// 自定义类别列表
/// </summary>
public List<CustomCategoryConfigItem> CustomCategories { get; set; } = new List<CustomCategoryConfigItem>();
}
/// <summary>
/// 自定义类别配置项(极简版,只用名称作为标识)
/// </summary>
public class CustomCategoryConfigItem
{
/// <summary>
/// 类别名称(同时作为显示名称和唯一标识)
/// </summary>
public string Name { get; set; }
}
/// <summary>

View File

@ -1090,7 +1090,7 @@ namespace NavisworksTransport
{
if (_customCategoryConfig == null)
{
_customCategoryConfig = Core.Config.CustomCategoryConfig.LoadFromFile();
_customCategoryConfig = new Core.Config.CustomCategoryConfig();
}
}
}
@ -1105,7 +1105,8 @@ namespace NavisworksTransport
{
lock (_customCategoryLock)
{
_customCategoryConfig = Core.Config.CustomCategoryConfig.LoadFromFile();
// 重新创建实例,从 ConfigManager 获取最新数据
_customCategoryConfig = new Core.Config.CustomCategoryConfig();
LogManager.Info("[CategoryAttributeManager] 自定义类别配置已重新加载");
}
}
@ -1171,8 +1172,14 @@ namespace NavisworksTransport
/// <returns>权重值未知类别返回1.0</returns>
public static double GetCategoryWeight(string categoryId)
{
var definition = GetCategoryDefinition(categoryId);
return definition?.Weight ?? 1.0;
// 简化为返回默认值,后续可根据需要扩展
// 障碍物类别返回最大权重
if (categoryId?.Equals("障碍物", StringComparison.OrdinalIgnoreCase) == true ||
categoryId?.Equals("空洞", StringComparison.OrdinalIgnoreCase) == true)
{
return double.MaxValue;
}
return 1.0;
}
/// <summary>
@ -1182,8 +1189,14 @@ namespace NavisworksTransport
/// <returns>是否可通行</returns>
public static bool IsCategoryTraversable(string categoryId)
{
var definition = GetCategoryDefinition(categoryId);
return definition?.Traversable ?? false;
// 简化为根据类别名称判断
if (categoryId?.Equals("障碍物", StringComparison.OrdinalIgnoreCase) == true ||
categoryId?.Equals("空洞", StringComparison.OrdinalIgnoreCase) == true ||
categoryId?.Equals("无关项", StringComparison.OrdinalIgnoreCase) == true)
{
return false;
}
return true;
}
/// <summary>

View File

@ -264,10 +264,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
/// <summary>
/// 可用类别筛选选项 - 从所有可用类别动态获取,并添加"全部"选项
/// 可用类别筛选选项 - 从所有可用类别动态获取
/// 注意:不包含"全部""全部"是UI层面的特殊选项
/// </summary>
public ThreadSafeObservableCollection<string> AvailableCategoryFilters { get; } =
new ThreadSafeObservableCollection<string>(new[] { "全部" });
new ThreadSafeObservableCollection<string>();
/// <summary>
/// 当前选中的类别筛选条件
@ -542,6 +543,25 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
});
}
// 处理自定义类别变更 - 刷新可用类别列表
if (category == ConfigCategory.All)
{
LogManager.Info("收到配置变更通知,正在更新可用类别列表...");
SafeExecute(() =>
{
// 重新加载自定义类别
CategoryAttributeManager.ReloadCustomCategories();
// 刷新可用类别列表
RefreshAvailableCategories();
string source = eventArgs.IsExternalChange ? "外部修改" : "内部更新";
UpdateMainStatus($"类别列表已更新 ({source})");
LogManager.Info($"可用类别列表已更新 - 来源: {source}, 当前类别数: {AvailableCategories.Count}");
});
}
}
catch (Exception ex)
{
@ -549,6 +569,58 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
/// <summary>
/// 刷新可用类别列表
/// </summary>
private void RefreshAvailableCategories()
{
_uiStateManager.ExecuteUIUpdateAsync(() =>
{
// 保存当前选中的类别
string previousSelectedCategory = SelectedCategory;
string previousSelectedFilter = SelectedCategoryFilter;
// 清空集合
AvailableCategories.Clear();
AvailableCategoryFilters.Clear();
// 先添加"全部"选项到筛选器
AvailableCategoryFilters.Add("全部");
// 从 CategoryAttributeManager 获取所有可用类别(内置 + 自定义)
var allCategories = CategoryAttributeManager.GetAllAvailableCategories().ToList();
foreach (var cat in allCategories)
{
AvailableCategories.Add(cat.DisplayName);
AvailableCategoryFilters.Add(cat.DisplayName);
}
// 恢复之前的选择(如果仍然存在)
if (!string.IsNullOrEmpty(previousSelectedCategory) &&
AvailableCategories.Contains(previousSelectedCategory))
{
SelectedCategory = previousSelectedCategory;
}
else if (AvailableCategories.Count > 0)
{
// 默认选择"通道"或第一个类别
SelectedCategory = AvailableCategories.Contains("通道") ? "通道" : AvailableCategories[0];
}
// 恢复筛选器选择
if (!string.IsNullOrEmpty(previousSelectedFilter) &&
AvailableCategoryFilters.Contains(previousSelectedFilter))
{
SelectedCategoryFilter = previousSelectedFilter;
}
else
{
SelectedCategoryFilter = "全部";
}
}).ConfigureAwait(false);
}
#endregion
#region - Command Pattern
@ -1302,6 +1374,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 初始化物流类别(包括内置和自定义类别)
AvailableCategories.Clear();
AvailableCategoryFilters.Clear();
// 先添加"全部"选项到筛选器
AvailableCategoryFilters.Add("全部");
// 从 CategoryAttributeManager 获取所有可用类别(内置 + 自定义)
@ -1321,6 +1395,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
SelectedCategory = AvailableCategories[0];
}
// 设置默认筛选器为"全部"
SelectedCategoryFilter = "全部";
});
// 注意:不在初始化时刷新选择状态和物流模型列表

View File

@ -658,22 +658,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
try
{
// 创建并显示配置编辑器对话框
var configEditorDialog = new NavisworksTransport.UI.WPF.Views.ConfigEditorDialog();
// 设置窗口所有者并显示对话框
DialogHelper.SetOwnerSafely(configEditorDialog);
bool? result = configEditorDialog.ShowDialog();
if (result == true)
// 使用单例模式显示配置编辑器(非模态,避免独占焦点)
// 不传递 owner让 ShowEditor 自己通过 DialogHelper 处理
var dialog = NavisworksTransport.UI.WPF.Views.ConfigEditorDialog.ShowEditor();
if (dialog != null)
{
UpdateMainStatus("配置已更新");
LogManager.Info("配置编辑器:用户保存了配置");
UpdateMainStatus("配置编辑器已打开");
LogManager.Info("配置编辑器:已打开(非模态)");
}
else
{
UpdateMainStatus("配置编辑已取消");
LogManager.Info("配置编辑器:用户取消了修改");
UpdateMainStatus("配置编辑器已在运行");
LogManager.Info("配置编辑器:窗口已存在并已激活");
}
}
catch (Exception ex)

View File

@ -20,8 +20,7 @@ NavisworksTransport 配置编辑器对话框 - 采用与主界面一致的Navisw
Height="700"
Width="900"
ResizeMode="CanResize"
WindowStartupLocation="CenterOwner"
Topmost="True"
WindowStartupLocation="CenterScreen"
MinHeight="500"
MinWidth="700">
@ -82,11 +81,6 @@ NavisworksTransport 配置编辑器对话框 - 采用与主界面一致的Navisw
<!-- 左侧工具按钮 -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Content="保存"
Click="SaveButton_Click"
Style="{StaticResource ActionButtonStyle}"
Margin="0,0,10,0"
ToolTip="保存配置文件"/>
<Button Content="重新加载"
Click="ReloadButton_Click"
Style="{StaticResource SecondaryButtonStyle}"

View File

@ -8,14 +8,27 @@ using NavisworksTransport.Core.Config;
namespace NavisworksTransport.UI.WPF.Views
{
/// <summary>
/// 配置编辑器对话框
/// 配置编辑器对话框 - 非模态窗口,避免独占焦点
/// </summary>
public partial class ConfigEditorDialog : Window
{
private string _originalContent;
private bool _isModified;
public ConfigEditorDialog()
/// <summary>
/// 当前显示的编辑器实例(单例模式)
/// </summary>
private static ConfigEditorDialog _currentInstance = null;
/// <summary>
/// 用于线程同步的锁对象
/// </summary>
private static readonly object _lock = new object();
/// <summary>
/// 私有构造函数,防止外部直接创建
/// </summary>
private ConfigEditorDialog()
{
InitializeComponent();
LoadConfigContent();
@ -94,7 +107,6 @@ namespace NavisworksTransport.UI.WPF.Views
{
try
{
string configPath = ConfigManager.ConfigFilePath;
string content = ConfigContentTextBox.Text;
// 验证 TOML 格式
@ -109,31 +121,26 @@ namespace NavisworksTransport.UI.WPF.Views
return false;
}
// 确保目录存在
Directory.CreateDirectory(ConfigManager.ConfigDirectory);
// 保存文件
File.WriteAllText(configPath, content, System.Text.Encoding.UTF8);
// 重新加载配置
ConfigManager.Instance.Reload();
// 使用 ConfigManager 的 ReloadFromContent 方法保存并加载
// 这个方法会临时禁用 FileSystemWatcher避免重复触发
ConfigManager.Instance.ReloadFromContent(content);
_originalContent = content;
_isModified = false;
ModifiedLabel.Content = "";
StatusLabel.Content = "配置已保存";
StatusLabel.Content = "配置已应用";
StatusLabel.Foreground = System.Windows.Media.Brushes.Green;
UpdateStatus();
LogManager.Info("配置文件已保存");
LogManager.Info("配置已应用");
return true;
}
catch (Exception ex)
{
LogManager.Error($"保存配置失败: {ex.Message}");
MessageBox.Show($"保存配置失败:\n{ex.Message}",
LogManager.Error($"应用配置失败: {ex.Message}");
MessageBox.Show($"应用配置失败:\n{ex.Message}",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
@ -141,11 +148,6 @@ namespace NavisworksTransport.UI.WPF.Views
// 事件处理器
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
SaveConfig();
}
private void ReloadButton_Click(object sender, RoutedEventArgs e)
{
if (_isModified)
@ -210,6 +212,9 @@ namespace NavisworksTransport.UI.WPF.Views
}
}
/// <summary>
/// 应用配置(保存但不关闭窗口)
/// </summary>
private void ApplyButton_Click(object sender, RoutedEventArgs e)
{
SaveConfig();
@ -221,13 +226,11 @@ namespace NavisworksTransport.UI.WPF.Views
{
if (SaveConfig())
{
DialogResult = true;
Close();
}
}
else
{
DialogResult = true;
Close();
}
}
@ -244,7 +247,6 @@ namespace NavisworksTransport.UI.WPF.Views
return;
}
DialogResult = false;
Close();
}
@ -266,5 +268,145 @@ namespace NavisworksTransport.UI.WPF.Views
UpdateStatus();
}
}
/// <summary>
/// 窗口关闭事件
/// </summary>
protected override void OnClosed(EventArgs e)
{
try
{
// 清除静态实例引用
lock (_lock)
{
if (_currentInstance == this)
{
_currentInstance = null;
LogManager.Info("配置编辑器对话框实例已从单例缓存中移除");
}
}
}
catch (Exception ex)
{
LogManager.Error($"清理配置编辑器对话框资源失败: {ex.Message}");
}
finally
{
base.OnClosed(e);
}
}
/// <summary>
/// 窗口加载事件 - 处理激活逻辑避免独占焦点
/// </summary>
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
try
{
// 激活并前置窗口,但不独占焦点
// 先置顶再取消置顶,让窗口前置但不系统级置顶
this.Activate();
this.Topmost = true;
this.Topmost = false;
LogManager.Debug("配置编辑器对话框已激活并前置显示");
}
catch (Exception ex)
{
LogManager.Error($"配置编辑器对话框初始化失败: {ex.Message}");
}
}
/// <summary>
/// 激活并显示已存在的窗口
/// </summary>
private void ActivateWindow()
{
try
{
// 如果窗口被最小化,则恢复
if (this.WindowState == WindowState.Minimized)
{
this.WindowState = WindowState.Normal;
}
// 激活并前置窗口(先置顶再取消置顶,避免独占焦点)
this.Activate();
this.Topmost = true;
this.Topmost = false;
LogManager.Info("配置编辑器对话框已激活并前置显示");
}
catch (Exception ex)
{
LogManager.Error($"激活配置编辑器对话框失败: {ex.Message}");
}
}
#region
/// <summary>
/// 创建并显示配置编辑器对话框(单例模式,非模态)
/// </summary>
/// <returns>对话框实例</returns>
public static ConfigEditorDialog ShowEditor()
{
try
{
lock (_lock)
{
// 如果当前已有实例且仍然打开,则激活窗口
if (_currentInstance != null)
{
try
{
// 检查窗口是否仍然有效
if (_currentInstance.IsLoaded && _currentInstance.IsVisible)
{
LogManager.Info("检测到已存在的配置编辑器窗口,激活窗口");
_currentInstance.ActivateWindow();
return _currentInstance;
}
else
{
// 窗口已关闭,清除引用
_currentInstance = null;
}
}
catch (Exception ex)
{
LogManager.Warning($"检查现有窗口状态时出现异常: {ex.Message},将创建新窗口");
_currentInstance = null;
}
}
// 创建新的对话框实例
var dialog = new ConfigEditorDialog();
// 使用 DialogHelper 安全地设置 owner
NavisworksTransport.Utils.DialogHelper.SetOwnerSafely(dialog);
// 设置为当前实例
_currentInstance = dialog;
// 使用 Show() 非模态显示,避免独占焦点
dialog.Show();
LogManager.Info("新的配置编辑器对话框已显示(非模态)");
return dialog;
}
}
catch (Exception ex)
{
LogManager.Error($"显示配置编辑器对话框失败: {ex.Message}");
MessageBox.Show($"显示配置编辑器对话框失败:\n{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
return null;
}
}
#endregion
}
}