Files
SzmediTools/Szmedi.RvKits/MEPTools/FacilityInfoProcessView.cs

779 lines
31 KiB
C#
Raw Normal View History

2025-12-23 21:37:02 +08:00
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
{
Items = package.ToList<FacilityItem>(1, configuration => configuration.SkipCastingErrors());
}
catch (EPPlus.Core.Extensions.Exceptions.ExcelValidationException)
{
MessageBox.Show("列名不存在或不匹配,请检查表头是否存在换行或多余文字。");
}
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);
FamilySymbol symbol = typeElem as FamilySymbol;
if (symbol != null)
{
Family family = symbol.Family;
// 确保族存在且未被添加过
if (family != null && !usedFamilyIds.Contains(family.Id))
{
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)
.Cast<Family>()
.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);
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)
{
string resultName = string.Empty;
double minRatio = 30;
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;
}
try
{
handler.Raise(
uiapp =>
{
var doc = uiapp.ActiveUIDocument.Document;
if (doc.IsFamilyDocument)
{
return;
}
doc.InvokeGroup(
ts =>
{
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.IsEditable)
{
sb.AppendLine($"{family.Name} 族不可编辑,跳过该族。");
}
Document famdoc = null;
try
{
famdoc = doc.EditFamily(family);
famdoc.Invoke(
ts =>
{
foreach (var paramItem in item.Parameters)
{
try
{
var familyParameters = famdoc.FamilyManager.GetParameters().Where(p => p.Definition.Name == paramItem.Name);
if (familyParameters.Any())
{
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}");
}
}
},
"添加缺失的族参数属性条目");
var loadedFamily = 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 != null && !param.IsReadOnly)
{
param.Set(paramItem.Value);
}
else
{
sb.AppendLine($"{instance.Id} - {instance.Name} 未找到参数 {paramItem.Name} 或参数只读,未设置其值。");
}
}
}
});
}
if (sb.Length > 0)
{
MessageBox.Show(sb.ToString());
}
else
{
MessageBox.Show("添加完成!");
}
});
});
}
catch (Exception)
{
throw;
}
}
[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; }
}
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}";
}
}
}