Files
SzmediTools/Szmedi.RvKits/MEPTools/FacilityInfoProcessViewModel.cs
2026-02-23 11:21:51 +08:00

778 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using Autodesk.Revit.DB;
using EPPlus.Core.Extensions;
using EPPlus.Core.Extensions.Attributes;
using FuzzySharp;
using Nice3point.Revit.Toolkit.External.Handlers;
using Nice3point.Revit.Toolkit.Helpers;
using OfficeOpenXml;
namespace Szmedi.RvKits.MEPTools
{
public partial class FacilityInfoProcessViewModel : ObservableObject
{
private readonly ActionEventHandler handler;
[ObservableProperty]
public partial string SearchText { get; set; }
partial void OnSearchTextChanged(string value)
{
FacilitiesCollectionView.Refresh();
}
[ObservableProperty]
public partial bool ContainsAllLines { get; set; }
partial void OnContainsAllLinesChanged(bool value)
{
FacilitiesCollectionView.Refresh();
}
[ObservableProperty]
public partial List<FacilityItem> Items { get; set; }
[ObservableProperty]
public partial string SearchInstanceText { get; set; }
partial void OnSearchInstanceTextChanged(string value)
{
InstancesCollectionView.Refresh();
}
private List<Family> Families { get; set; } = [];
public ICollectionView FacilitiesCollectionView { get; set; }
public ICollectionView InstancesCollectionView { get; set; }
public FacilityInfoProcessViewModel()
{
GlobalVariables.UIApplication?.ViewActivated += UIApplication_ViewActivated;
handler = new ActionEventHandler();
}
[RelayCommand]
private void ProcessExcel(string filePath)
{
FileInfo fi = new(filePath);
using ExcelPackage package = new(fi);
AppDomain.CurrentDomain.AssemblyResolve += ResolveHelper.ResolveAssembly;
try
{
var resultList = package.ToList<FacilityItem>(1, configuration => configuration.SkipCastingErrors());
Items = resultList.Where(f => !string.IsNullOrEmpty(f.Name)).ToList();
}
catch (EPPlus.Core.Extensions.Exceptions.ExcelValidationException ex)
{
MessageBox.Show("列名不存在或不匹配,请检查表头是否存在换行或多余文字。", ex.Message);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
AppDomain.CurrentDomain.AssemblyResolve -= ResolveHelper.ResolveAssembly;
}
}
private void UIApplication_ViewActivated(object sender, Autodesk.Revit.UI.Events.ViewActivatedEventArgs e)
{
GetFamilies();
FuzzyMatch();
}
//private Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args)
//{
// if (args.Name.Contains("ComponentModel.Annotations"))
// {
// var assemblyFile = Path.Combine(GlobalVariables.DirAssembly, "System.ComponentModel.Annotations.dll");
// return File.Exists(assemblyFile) ? Assembly.LoadFrom(assemblyFile) : null;
// }
// if (args.Name.Contains("EPPlus"))
// {
// var epplusFile = Path.Combine(GlobalVariables.DirAssembly, "EPPlus.dll");
// return File.Exists(epplusFile) ? Assembly.LoadFrom(epplusFile) : null;
// }
// return null;
//}
public List<Family> GetUsedFamilies(Document doc)
{
// 获取所有实例元素这里不需要由ToElements()转化全部对象,节省内存,仅遍历)
var instances = doc.OfClass<FamilyInstance>();
// 3. 收集所有被使用的“类型ID” (使用HashSet自动去重)
HashSet<ElementId> usedTypeIds = new HashSet<ElementId>();
foreach (Element elem in instances)
{
ElementId typeId = elem.GetTypeId();
if (typeId != ElementId.InvalidElementId)
{
usedTypeIds.Add(typeId);
}
}
// 4. 从类型反向获取族 (使用HashSet再次去重族ID防止多个类型属于同一个族)
HashSet<ElementId> usedFamilyIds = new HashSet<ElementId>();
List<Family> resultFamilies = new List<Family>();
foreach (ElementId typeId in usedTypeIds)
{
// 获取类型元素 (FamilySymbol)
Element typeElem = doc.GetElement(typeId);
if (typeElem is FamilySymbol symbol)
{
Family family = symbol.Family;
// 确保族存在且未被添加过
if (family != null && usedFamilyIds.Add(family.Id))
{
resultFamilies.Add(family);
}
}
}
return resultFamilies;
}
private void GetFamilies()
{
var doc = GlobalVariables.UIApplication.ActiveUIDocument.Document;
if (doc.IsFamilyDocument)
{
return;
}
var families = GetUsedFamilies(doc)
.Where(
fam => fam.FamilyCategory.AllowsBoundParameters &&
fam.IsEditable &&
fam.FamilyCategory.CategoryType == CategoryType.Model &&
!Enum.GetName(typeof(BuiltInCategory), fam.FamilyCategoryId.IntegerValue)!.Contains(
"Fitting") &&
fam.FamilyCategoryId.IntegerValue != -2008013);//风道末端
Families = [.. families];
}
[ObservableProperty]
public partial string ExcelPath { get; set; }
[RelayCommand]
private void BrowserExcel()
{
var ofd = new Microsoft.Win32.OpenFileDialog
{
Filter = "Excel 文件 (*.xlsx;*.xls)|*.xlsx;*.xls",
Title = "选择 Excel 文件"
};
bool? result = ofd.ShowDialog();
if (result == true)
{
GetFamilies();
ExcelPath = ofd.FileName;
ProcessExcel(ofd.FileName);
if (Items == null)
{
return;
}
FuzzyMatch();
FacilitiesCollectionView = CollectionViewSource.GetDefaultView(Items);
FacilitiesCollectionView.Filter = o =>
{
if (string.IsNullOrEmpty(SearchText))
return true;
if (o is FacilityItem info)
{
//var title = GlobalVariables.UIApplication.ActiveUIDocument.Document.Title;
//var match = Regex.Match(title, @"(?<=^\d+号线-)([^-]+?)(?=车辆段|站|区间|停车场|主变电|变电所)");
//string result = match.Success ? match.Value : string.Empty;
return info.ToString().Contains(SearchText) || (ContainsAllLines && info.ToString().Contains("全线"));
}
return false;
};
}
}
private void FuzzyMatch()
{
if (Items == null || Families == null)
{
return;
}
foreach (var item in Items)
{
double minRatio = 30;
if(string.IsNullOrEmpty(item.Name))
{
continue;
}
foreach (var family in Families)
{
var famName = family.Name;
var tempRatio = Fuzz.TokenSetRatio(famName, item.Name);
if (tempRatio >= minRatio)
{
minRatio = tempRatio;
item.MappedFamily = family;
}
}
}
}
public bool? IsAllItemsSelected
{
get
{
var selected = Items?.Select(item => item.IsSelected).Distinct().ToList();
if (selected == null)
{
return false;
}
return selected.Count == 1 ? selected.Single() : null;
}
set
{
if (value.HasValue)
{
SelectAll(value.Value, Items);
OnPropertyChanged();
}
}
}
private static void SelectAll(bool select, IEnumerable<FacilityItem> models)
{
foreach (var model in models)
{
model.IsSelected = select;
}
}
[NotifyCanExecuteChangedFor(nameof(SelectInstanceCommand))]
[ObservableProperty]
private partial bool CanSelecting { get; set; } = true;
/// <summary>
/// 提取字符串中所有中文字符
/// </summary>
private static string ExtractChinese(string text)
{
if (string.IsNullOrEmpty(text)) return string.Empty;
// 先将非中文替换为空格,再将连续空格替换为单个空格,最后去除首尾空格
var result = Regex.Replace(text, @"[^\u4e00-\u9fa5]", " ");
result = Regex.Replace(result, @"\s+", " ").Trim();
return result;
}
[RelayCommand(CanExecute = nameof(CanSelecting))]
private void SelectInstance(FacilityItem item)
{
CanSelecting = false;
try
{
var uidoc = GlobalVariables.UIApplication.ActiveUIDocument;
var reference = uidoc.Selection
.PickObject(
Autodesk.Revit.UI.Selection.ObjectType.Element,
new FuncFilter(
e => e is FamilyInstance instance &&
instance.Category != null &&
!instance.Symbol.Family.IsCurtainPanelFamily &&
instance.Symbol.Family.IsUserCreated &&
instance.Symbol.Family.IsEditable &&
!Enum.GetName(typeof(BuiltInCategory), instance.Category.Id.IntegerValue)!.Contains("Fitting") &&
instance.Category.CategoryType == CategoryType.Model &&
instance.Category.Id.IntegerValue != -2008013),
"请选择关联的族实例");
var instance = uidoc.Document.GetElement(reference) as FamilyInstance;
item.MappedFamily = instance.Symbol.Family;
}
catch (Autodesk.Revit.Exceptions.OperationCanceledException)
{
}
finally
{
CanSelecting = true;
}
}
public static string ValidateDuplicatesWithDisplay<T>(
IEnumerable<T> items,
Func<T, object> keySelector, // 用于判断重复的键(如 ID
Func<T, string> displaySelector // 用于显示的文本(如 Name 或组合)
)
{
if (items == null)
return "数据为空。";
var keyToDisplay = new Dictionary<object, string>(new ObjectKeyComparer());
var duplicates = new List<string>();
foreach (var item in items)
{
if (item == null) continue;
var key = keySelector(item);
var display = displaySelector(item) ?? "(空)";
// 如果键已存在,说明重复,记录显示文本
if (keyToDisplay.TryGetValue(key, out var existingDisplay))
{
// 避免重复添加相同项的提示(可选)
if (!duplicates.Contains(display) && !duplicates.Contains(existingDisplay))
{
duplicates.Add($"[{existingDisplay} 与 {display}]");
}
}
else
{
keyToDisplay[key] = display;
}
}
if (duplicates.Count > 0)
return $"发现重复的项,可能包含:{string.Join("", duplicates)}";
return string.Empty;
}
// 辅助类:解决 object 作为 key 时的 null 和类型一致性问题
class ObjectKeyComparer : IEqualityComparer<object>
{
public new bool Equals(object x, object y)
{
if (x == null && y == null) return true;
if (x == null || y == null) return false;
return x.Equals(y);
}
public int GetHashCode(object obj) => obj?.GetHashCode() ?? 0;
}
[RelayCommand]
private void ProcessModelInfo()
{
var processItems = Items.Where(i => i.IsSelected && i.MappedFamily != null && i.Instances.Count > 0).ToList();
if (processItems.Count == 0)
{
MessageBox.Show("未选择任何有效的设备项进行处理。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
handler.Raise(
uiapp =>
{
var doc = uiapp.ActiveUIDocument.Document;
if (doc.IsFamilyDocument)
{
return;
}
doc.InvokeGroup(
_ =>
{
StringBuilder sb = new StringBuilder();
var allInstances = Items.Where(i => i.IsSelected).SelectMany(i => i.Instances).ToList();
string message = ValidateDuplicatesWithDisplay(allInstances, s => s.Id, s => $"{s.Symbol.FamilyName}:{s.Name}");
if (!string.IsNullOrEmpty(message))
{
MessageBox.Show(message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
foreach (var item in processItems)
{
var family = item.MappedFamily;
if (!family.IsValidObject)
{
sb.AppendLine($"<{item.Name}>因文档手动修改导致匹配族失效,请重新匹配,跳过该族。");
continue;
}
if (!family.IsEditable)
{
sb.AppendLine($"{family.Name} 族不可编辑,跳过该族。");
continue;
}
Document famdoc = null;
try
{
famdoc = doc.EditFamily(family);
famdoc.Invoke(
_ =>
{
foreach (var paramItem in item.Parameters)
{
try
{
var familyParameters = famdoc.FamilyManager.GetParameters()
.Where(p => p.Definition.Name == paramItem.Name).ToList();
if (familyParameters.Count > 0)
{
var parameter = familyParameters.FirstOrDefault();
if (parameter.Definition is InternalDefinition def && def.BuiltInParameter == BuiltInParameter.INVALID)
{
famdoc.FamilyManager.RemoveParameter(parameter);
famdoc.Regenerate();
}
famdoc.FamilyManager.AddParameter(paramItem.Name, BuiltInParameterGroup.PG_TEXT, ParameterType.Text, true);
}
else
{
famdoc.FamilyManager
.AddParameter(
paramItem.Name,
BuiltInParameterGroup.PG_TEXT,
ParameterType.Text,
true);
}
}
catch (Exception ex)
{
sb.AppendLine($"{family.Name} 族参数 {paramItem.Name} 添加失败,{ex.Message}");
}
}
},
"添加缺失的族参数属性条目");
item.MappedFamily = famdoc.LoadFamily(doc, new FamilyLoadOption());
}
catch (Exception ex)
{
sb.AppendLine($"{family.Name} 族加载到项目中失败,原因:{ex.Message}");
}
finally
{
famdoc?.Close(false);
}
doc.Invoke(
ts =>
{
foreach (var instance in item.Instances)
{
foreach (var paramItem in item.Parameters)
{
Parameter param = instance.GetParameters(paramItem.Name)?.FirstOrDefault();
if (param is { IsReadOnly: false })
{
param.Set(paramItem.Value);
}
else
{
sb.AppendLine($"{instance.Id} - {instance.Name} 未找到参数 {paramItem.Name} 或参数只读,未设置其值。");
}
}
}
});
}
MessageBox.Show(sb.Length > 0 ? sb.ToString() : "添加完成!");
}, "处理设备信息");
});
}
[ObservableProperty]
public partial FacilityItem SelectedFacility { get; set; }
partial void OnSelectedFacilityChanged(FacilityItem value)
{
if (value != null)
{
InstancesCollectionView = CollectionViewSource.GetDefaultView(value.Instances);
InstancesCollectionView.Filter = o =>
{
if (string.IsNullOrEmpty(SearchInstanceText))
return true;
if (o is FamilyInstance ins)
{
return ins.Name.Contains(SearchInstanceText);
}
return false;
};
}
}
[RelayCommand]
private void LocationInstance(FamilyInstance instance)
{
var uidoc = GlobalVariables.UIApplication.ActiveUIDocument;
if (uidoc.Document.GetElement(instance.Id).IsValidObject)
{
uidoc.Selection.SetElementIds([instance.Id]);
uidoc.ShowElements(instance);
}
}
[RelayCommand]
private void RemoveInstance(FamilyInstance instance)
{
SelectedFacility?.Instances.Remove(instance);
}
[RelayCommand]
private void RemoveSelectedInstances(IList list)
{
if (SelectedFacility?.Instances == null || list == null)
return;
//先找出所有需要移除的项(不修改原集合)
var toRemove = new List<FamilyInstance>();
foreach (var item in list)
{
if (item is FamilyInstance instance &&
SelectedFacility.Instances.Contains(instance))
{
toRemove.Add(instance);
}
}
foreach (var instance in toRemove)
{
SelectedFacility.Instances.Remove(instance);
}
}
[RelayCommand]
private void SelectSelectedInstances(IList list)
{
if (list != null && list.Count > 0)
{
var candidates = new HashSet<FamilyInstance>(
list.Cast<object>().Where(x => x is FamilyInstance).Cast<FamilyInstance>());
var uidoc = GlobalVariables.UIApplication.ActiveUIDocument;
uidoc.Selection.SetElementIds(new HashSet<ElementId>(candidates.Select(i => i.Id)));
}
}
[RelayCommand]
private void SelectAllInstances()
{
if (SelectedFacility?.Instances.Count > 0)
{
var uidoc = GlobalVariables.UIApplication.ActiveUIDocument;
uidoc.Selection.SetElementIds(new HashSet<ElementId>(SelectedFacility.Instances
.Select(i => i.Id)));
}
}
[RelayCommand]
private void RemoveAllInstances()
{
SelectedFacility?.Instances.Clear();
}
[RelayCommand]
private void RepickSamplingInstances(Family family)
{
if (family == null || !family.IsValidObject)
{
return;
}
var uidoc = GlobalVariables.UIApplication.ActiveUIDocument;
try
{
while (true)
{
var excludedIds = new HashSet<int>(SelectedFacility.Instances.Select(i => i.Id.IntegerValue));
var reference = uidoc.Selection
.PickObject(
Autodesk.Revit.UI.Selection.ObjectType.Element,
new FuncFilter(
e => e is FamilyInstance fi &&
fi.Symbol.Family.Id.IntegerValue == family.Id.IntegerValue &&
!excludedIds.Contains(e.Id.IntegerValue)),
"请选择关联的族实例");
var instance = uidoc.Document.GetElement(reference) as FamilyInstance;
SelectedFacility?.Instances.Add(instance);
}
}
catch (Autodesk.Revit.Exceptions.OperationCanceledException)
{
}
}
[RelayCommand]
private void RepickInstancesByRectangle(Family family)
{
if (family == null || !family.IsValidObject)
{
return;
}
var uidoc = GlobalVariables.UIApplication.ActiveUIDocument;
try
{
var excludedIds = new HashSet<int>(SelectedFacility.Instances.Select(i => i.Id.IntegerValue));
var instances = uidoc.Selection
.PickElementsByRectangle(
new FuncFilter(
e => e is FamilyInstance fi &&
fi.Symbol.Family.Id.IntegerValue == family.Id.IntegerValue
&& !excludedIds.Contains(e.Id.IntegerValue)),
"请选择关联的族实例").Cast<FamilyInstance>();
foreach (var item in instances)
{
SelectedFacility?.Instances.Add(item);
}
}
catch (Autodesk.Revit.Exceptions.OperationCanceledException)
{
}
}
~FacilityInfoProcessViewModel()
{
if (GlobalVariables.UIApplication.IsValidObject)
{
GlobalVariables.UIApplication.ViewActivated -= UIApplication_ViewActivated;
}
}
}
public partial class ParameterItem : ObservableObject
{
public ParameterItem(string name, string value = "")
{
Name = name;
Value = value;
}
public ParameterItem()
{
}
[ObservableProperty]
public partial string Name { get; set; }
[ObservableProperty]
public partial string Value { get; set; }
}
/// <summary>
/// Excel表里的设备项
/// </summary>
public partial class FacilityItem : ObservableObject
{
public FacilityItem()
{
if (!string.IsNullOrEmpty(ParameterString))
{
Parameters = ProcessParameters(ParameterString);
}
}
[ObservableProperty]
public partial bool IsSelected { get; set; }
[ExcelTableColumn("设备")]
public string Name { get; set; }
[ExcelTableColumn("部署范围")]
public string Stations { get; set; }
[ExcelTableColumn("设备参数")]
[ObservableProperty]
public partial string ParameterString { get; set; }
partial void OnParameterStringChanged(string value)
{
if (!string.IsNullOrEmpty(value))
{
Parameters = ProcessParameters(value);
}
}
[ObservableProperty]
public partial string SearchText { get; set; }
public ICollectionView CollectionView { get; private set; }
public ObservableCollection<FamilyInstance> Instances { get; set; } = [];
public string Brand { get; set; }
public string Specification { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayText))]
public partial Family MappedFamily { get; set; } = null;
partial void OnMappedFamilyChanged(Family value)
{
var doc = value.Document;
Instances.Clear();
List<FamilyInstance> instances = [..doc.ActiveView.QueryInstancesByType<FamilyInstance>()
.Cast<FamilyInstance>()
.Where(fi => fi.Symbol.Family.Id.IntegerValue == value.Id.IntegerValue)];
foreach (var instance in instances)
{
Instances.Add(instance);
}
CollectionView = CollectionViewSource.GetDefaultView(Instances);
CollectionView.Filter = o =>
{
if (string.IsNullOrEmpty(SearchText))
return true;
if (o is FamilyInstance ins)
{
//var title = GlobalVariables.UIApplication.ActiveUIDocument.Document.Title;
//var match = Regex.Match(title, @"(?<=^\d+号线-)([^-]+?)(?=车辆段|站|区间|停车场|主变电|变电所)");
//string result = match.Success ? match.Value : string.Empty;
return ins.Name.Contains(SearchText);
}
return false;
};
}
public string DisplayText => MappedFamily switch
{
null => "未匹配",
_ => $"{MappedFamily?.Name ?? ""}"
};
[ObservableProperty]
public partial string ErrorMessage { get; set; }
[ObservableProperty]
public partial List<ParameterItem> Parameters { get; set; }
private List<ParameterItem> ProcessParameters(string input)
{
List<ParameterItem> parameters = [];
//string pattern = @"^([^:]+):(.*)$";
string pattern = @"^([^:]+)[:](.*)$";
MatchCollection matches = Regex.Matches(input, pattern, RegexOptions.Multiline);
foreach (Match match in matches)
{
// Group[1] 是冒号前的内容 (参数名)
string paramName = match.Groups[1].Value.Trim();
if (string.IsNullOrEmpty(paramName))
{
continue;
}
string paramValue = match.Groups[2].Value;
if (string.IsNullOrEmpty(paramValue))
{
continue;
}
if (paramName == "品牌")
{
Brand = paramValue;
}
if (paramName == "设备型号")
{
Specification = paramValue;
}
// Group[2] 是冒号后的所有内容 (参数值)
var parameterItem = new ParameterItem
{
Name = paramName,
Value = paramValue
};
var isExist = parameters.Any(p => p.Name == paramName);
if (isExist)
{
ErrorMessage += $"{paramName}参数名重复\n";
}
else
{
parameters.Add(parameterItem);
}
}
return parameters;
}
public override string ToString()
{
return $"{Name} {Brand} {Specification} {Stations}";
}
}
}