diff --git a/src/RevitParamsChecker/Models/Filtration/Filter.cs b/src/RevitParamsChecker/Models/Filtration/Filter.cs index 8e76659f8..2563a418a 100644 --- a/src/RevitParamsChecker/Models/Filtration/Filter.cs +++ b/src/RevitParamsChecker/Models/Filtration/Filter.cs @@ -6,5 +6,15 @@ public Filter() { public string Name { get; set; } + /// + /// Сериализованный контекст фильтра по параметрам элементов. + /// Пустое значение означает, что фильтрация не задана. + /// public string FilterContext { get; set; } + + /// + /// Сериализованный контекст фильтра по параметрам материалов элементов. + /// Пустое значение означает, что фильтрация по материалам не задана. + /// + public string MaterialsFilterContext { get; set; } } diff --git a/src/RevitParamsChecker/Models/Filtration/FilterDataProvider.cs b/src/RevitParamsChecker/Models/Filtration/FilterDataProvider.cs index 0f0ff7824..73062798c 100644 --- a/src/RevitParamsChecker/Models/Filtration/FilterDataProvider.cs +++ b/src/RevitParamsChecker/Models/Filtration/FilterDataProvider.cs @@ -27,6 +27,14 @@ public DataProvider CreateDataProvider() { return new DataProvider(GetCategories(), GetParams, GetParamValues); } + /// + /// Создает провайдер данных для фильтра по параметрам материалов элементов. + /// + /// Провайдер данных с единственной категорией - "Материалы". + public DataProvider CreateMaterialsDataProvider() { + return new DataProvider(GetMaterialsCategories(), GetMaterialParams, GetMaterialParamValues); + } + private ICollection GetParams(ICollection categories) { return ParameterFilterUtilities .GetFilterableParametersInCommon(_revitRepository.Document, [..categories.Select(c => c.Id)]) @@ -43,6 +51,52 @@ private ICollection GetCategories() { .ToArray(); } + private ICollection GetMaterialsCategories() { + var category = Category.GetCategory(_revitRepository.Document, BuiltInCategory.OST_Materials); + return category is null ? [] : [category]; + } + + /// + /// Возвращает параметры, доступные для фильтрации материалов. + /// + /// + /// Материалы не входят в фильтруемые категории видов Revit, поэтому, + /// если получить параметры штатным способом не удалось, + /// они берутся из первого попавшегося материала документа. + /// + private ICollection GetMaterialParams(ICollection categories) { + ICollection paramIds; + try { + paramIds = ParameterFilterUtilities.GetFilterableParametersInCommon( + _revitRepository.Document, + [..categories.Select(c => c.Id)]); + } catch(Autodesk.Revit.Exceptions.ApplicationException) { + paramIds = []; + } + + var revitParams = paramIds + .Select(GetFilterableParam) + .Where(p => p != null) + .ToArray(); + return revitParams.Length > 0 ? revitParams : GetFirstMaterialParams(); + } + + private ICollection GetFirstMaterialParams() { + var material = new FilteredElementCollector(_revitRepository.Document) + .OfClass(typeof(Material)) + .FirstElement(); + if(material is null) { + return []; + } + + return material.Parameters + .OfType() + .Select(p => p.Id) + .Select(GetFilterableParam) + .Where(p => p != null) + .ToArray(); + } + private RevitParam GetFilterableParam(ElementId paramId) { try { if(paramId.IsSystemId()) { @@ -99,4 +153,23 @@ private ICollection GetParamValues( return []; } } + + private ICollection GetMaterialParamValues(ICollection categories, RevitParam param) { + return _revitRepository.GetDocuments() + .SelectMany(d => GetMaterialParamValues(d.Document, param)) + .Distinct() + .ToArray(); + } + + private ICollection GetMaterialParamValues(Document doc, RevitParam param) { + try { + return new FilteredElementCollector(doc) + .OfClass(typeof(Material)) + .Where(e => e.IsExistsParamValue(param.Name)) + .Select(e => e.GetParamValueString(param)) + .ToArray(); + } catch(Autodesk.Revit.Exceptions.ApplicationException) { + return []; + } + } } diff --git a/src/RevitParamsChecker/Models/Revit/RevitRepository.cs b/src/RevitParamsChecker/Models/Revit/RevitRepository.cs index 55bd2cbfa..17b8833ed 100644 --- a/src/RevitParamsChecker/Models/Revit/RevitRepository.cs +++ b/src/RevitParamsChecker/Models/Revit/RevitRepository.cs @@ -6,7 +6,9 @@ using Autodesk.Revit.DB; using Autodesk.Revit.UI; +using Bim4Everyone.RevitFiltration; using Bim4Everyone.RevitFiltration.Controls; +using Bim4Everyone.RevitFiltration.Extensions.Materials; using RevitParamsChecker.Models.Filtration; @@ -14,11 +16,17 @@ namespace RevitParamsChecker.Models.Revit; internal class RevitRepository { private readonly IFilterContextParser _filterParser; + private readonly ILogicalFilterFactory _filterFactory; private DocumentModel[] _documentsCache; private readonly Bim4Everyone.RevitFiltration.Options _filterBuildOptions = new() { Tolerance = 0.001 }; + private readonly MaterialOptions _materialsBuildOptions = new() { Tolerance = 0.001 }; - public RevitRepository(UIApplication uiApplication, IFilterContextParser filterParser) { + public RevitRepository( + UIApplication uiApplication, + IFilterContextParser filterParser, + ILogicalFilterFactory filterFactory) { _filterParser = filterParser ?? throw new ArgumentNullException(nameof(filterParser)); + _filterFactory = filterFactory ?? throw new ArgumentNullException(nameof(filterFactory)); UIApplication = uiApplication ?? throw new ArgumentNullException(nameof(uiApplication)); } @@ -57,10 +65,12 @@ public ICollection GetElements(DocumentModel doc, Filter filter) { var paramsFilter = context!.GetFilter().Build(doc.Document, _filterBuildOptions); var categoriesFilter = new ElementMulticategoryFilter(context.SelectedCategories); + var materialsFilter = GetMaterialsFilter(doc.Document, filter.MaterialsFilterContext); return new FilteredElementCollector(doc.Document) .WhereElementIsNotElementType() .WherePasses(categoriesFilter) .WherePasses(paramsFilter) + .PassesFilter(materialsFilter) .Select(e => new ElementModel( e, doc.IsLink ? new Reference(e).CreateLinkReference(doc.Link) : new Reference(e))) @@ -104,6 +114,24 @@ public void SelectElements(ICollection elements) { #endif } + /// + /// Создает фильтр элементов по параметрам их материалов. + /// + /// Документ, в котором отбираются материалы. + /// Сериализованный контекст фильтра по материалам. + /// + /// Фильтр по материалам. Если контекст не задан либо его не удалось прочитать, + /// возвращается фильтр без правил, пропускающий все элементы. + /// + private MaterialsFilter GetMaterialsFilter(Document doc, string serializedContext) { + if(!string.IsNullOrWhiteSpace(serializedContext) + && _filterParser.TryParse(serializedContext, out var context)) { + return context!.GetFilter().BuildMaterialsFilter(doc, _materialsBuildOptions); + } + + return _filterFactory.CreateAndFilter().BuildMaterialsFilter(doc, _materialsBuildOptions); + } + private ICollection GetRevitLinkInstances() { return new FilteredElementCollector(Document) .OfClass(typeof(RevitLinkInstance)) diff --git a/src/RevitParamsChecker/RevitParamsCheckerCommand.cs b/src/RevitParamsChecker/RevitParamsCheckerCommand.cs index 9609a978a..43a9ffcfe 100644 --- a/src/RevitParamsChecker/RevitParamsCheckerCommand.cs +++ b/src/RevitParamsChecker/RevitParamsCheckerCommand.cs @@ -6,7 +6,6 @@ using Autodesk.Revit.Attributes; using Autodesk.Revit.UI; -using Bim4Everyone.RevitFiltration.Controls; using Bim4Everyone.RevitFiltration.Ninject; using dosymep.Bim4Everyone; @@ -62,8 +61,8 @@ protected override void Execute(UIApplication uiApplication) { kernel.UseLogicalFilterFactory(); kernel.UseLogicalFilterProviderFactory(); kernel.UseFilterContextParser(); - kernel.Bind() - .ToMethod(c => new FilterDataProvider(c.Kernel.Get()).CreateDataProvider()) + kernel.Bind() + .ToSelf() .InSingletonScope(); kernel.Bind() .To() diff --git a/src/RevitParamsChecker/ViewModels/Filtration/FilterViewModel.cs b/src/RevitParamsChecker/ViewModels/Filtration/FilterViewModel.cs index dcde69121..63d973d86 100644 --- a/src/RevitParamsChecker/ViewModels/Filtration/FilterViewModel.cs +++ b/src/RevitParamsChecker/ViewModels/Filtration/FilterViewModel.cs @@ -11,15 +11,29 @@ namespace RevitParamsChecker.ViewModels.Filtration; internal class FilterViewModel : BaseViewModel, IEquatable, IName { private readonly Guid _guid; + private readonly IFilterContextParser _filterContextParser; + private string _savedFilterContext; + private string _savedMaterialsFilterContext; private string _name; private bool _modified; - public FilterViewModel(string name, ILogicalFilterProvider filterProvider) { + public FilterViewModel( + string name, + ILogicalFilterProvider filterProvider, + ILogicalFilterProvider materialsFilterProvider, + IFilterContextParser filterContextParser) { if(string.IsNullOrWhiteSpace(name)) { throw new ArgumentException(nameof(name)); } FilterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider)); + MaterialsFilterProvider = materialsFilterProvider + ?? throw new ArgumentNullException(nameof(materialsFilterProvider)); + _filterContextParser = filterContextParser ?? throw new ArgumentNullException(nameof(filterContextParser)); + _savedFilterContext = GetContext(FilterProvider); + _savedMaterialsFilterContext = GetContext(MaterialsFilterProvider); + FilterProvider.FilterContextChanged += OnFilterContextChanged; + MaterialsFilterProvider.FilterContextChanged += OnFilterContextChanged; Name = name; Modified = true; _guid = Guid.NewGuid(); @@ -36,8 +50,25 @@ public bool Modified { set => RaiseAndSetIfChanged(ref _modified, value); } + /// + /// Провайдер фильтра по параметрам элементов. + /// public ILogicalFilterProvider FilterProvider { get; } + /// + /// Провайдер фильтра по параметрам материалов элементов. + /// + public ILogicalFilterProvider MaterialsFilterProvider { get; } + + /// + /// Запоминает текущее состояние фильтров как сохраненное и снимает пометку об изменениях. + /// + public void AcceptChanges() { + _savedFilterContext = GetContext(FilterProvider); + _savedMaterialsFilterContext = GetContext(MaterialsFilterProvider); + Modified = false; + } + public override bool Equals(object obj) { return Equals(obj as FilterViewModel); } @@ -58,6 +89,39 @@ public bool Equals(FilterViewModel other) { return _guid == other._guid; } + /// + /// Помечает набор измененным, если фильтр действительно отличается от сохраненного. + /// + /// + /// Контрол пересобирает контекст фильтра при загрузке провайдера, то есть при каждом выборе набора в списке, + /// поэтому одного факта события недостаточно: состояния сравниваются в том же виде, в котором сохраняются. + /// + private void OnFilterContextChanged(object sender, FilterContextChangedEventArgs e) { + if(IsFilterChanged()) { + Modified = true; + } + } + + private bool IsFilterChanged() { + return !string.Equals( + _savedFilterContext, + GetContext(FilterProvider), + StringComparison.CurrentCultureIgnoreCase) + || !string.Equals( + _savedMaterialsFilterContext, + GetContext(MaterialsFilterProvider), + StringComparison.CurrentCultureIgnoreCase); + } + + /// + /// Возвращает сериализованный контекст фильтра либо null, если фильтр задан некорректно. + /// + private string GetContext(ILogicalFilterProvider provider) { + return provider.CanGetFilter(out _) + ? _filterContextParser.Serialize(provider.GetFilter()) + : null; + } + private void OnModelPropertyChanged(object sender, PropertyChangedEventArgs e) { if(e.PropertyName == nameof(Name)) { Modified = true; diff --git a/src/RevitParamsChecker/ViewModels/Filtration/FiltrationPageViewModel.cs b/src/RevitParamsChecker/ViewModels/Filtration/FiltrationPageViewModel.cs index 45e460b06..1f896cc41 100644 --- a/src/RevitParamsChecker/ViewModels/Filtration/FiltrationPageViewModel.cs +++ b/src/RevitParamsChecker/ViewModels/Filtration/FiltrationPageViewModel.cs @@ -7,6 +7,9 @@ using System.Linq; using System.Windows.Input; +using Autodesk.Revit.DB; + +using Bim4Everyone.RevitFiltration; using Bim4Everyone.RevitFiltration.Controls; using dosymep.SimpleServices; @@ -21,8 +24,10 @@ namespace RevitParamsChecker.ViewModels.Filtration; internal class FiltrationPageViewModel : BaseViewModel { private readonly ILocalizationService _localization; private readonly ILogicalFilterProviderFactory _filterProviderFactory; + private readonly ILogicalFilterFactory _filterFactory; private readonly IFilterContextParser _filterContextParser; private readonly DataProvider _dataProvider; + private readonly DataProvider _materialsDataProvider; private readonly FiltersRepository _filtersRepo; private readonly FiltersConverter _filtersConverter; private readonly NamesService _namesService; @@ -38,8 +43,9 @@ public FiltrationPageViewModel( ISaveFileDialogService saveFileDialogService, IMessageBoxService messageBoxService, ILogicalFilterProviderFactory filterProviderFactory, + ILogicalFilterFactory filterFactory, IFilterContextParser filterContextParser, - DataProvider dataProvider, + FilterDataProvider dataProviderFactory, FiltersRepository filtersRepo, FiltersConverter filtersConverter, NamesService namesService) { @@ -50,8 +56,14 @@ public FiltrationPageViewModel( _localization = localization ?? throw new ArgumentNullException(nameof(localization)); _filterProviderFactory = filterProviderFactory ?? throw new ArgumentNullException(nameof(filterProviderFactory)); + _filterFactory = filterFactory ?? throw new ArgumentNullException(nameof(filterFactory)); _filterContextParser = filterContextParser ?? throw new ArgumentNullException(nameof(filterContextParser)); - _dataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider)); + if(dataProviderFactory is null) { + throw new ArgumentNullException(nameof(dataProviderFactory)); + } + + _dataProvider = dataProviderFactory.CreateDataProvider(); + _materialsDataProvider = dataProviderFactory.CreateMaterialsDataProvider(); _filtersRepo = filtersRepo ?? throw new ArgumentNullException(nameof(filtersRepo)); _filtersConverter = filtersConverter ?? throw new ArgumentNullException(nameof(filtersConverter)); _namesService = namesService ?? throw new ArgumentNullException(nameof(namesService)); @@ -104,7 +116,11 @@ private void AddFilter() { string newName = _namesService.CreateNewName( _localization.GetLocalizedString("FiltersPage.NewFilterPrompt"), Filters.Select(f => f.Name).ToArray()); - var vm = new FilterViewModel(newName, GetFilterProvider()); + var vm = new FilterViewModel( + newName, + GetFilterProvider(), + GetMaterialsFilterProvider(), + _filterContextParser); vm.PropertyChanged += OnFilterChanged; Filters.Add(vm); SelectedFilter = vm; @@ -134,7 +150,9 @@ private void CopyFilter(FilterViewModel filter) { Filters.Select(f => f.Name).ToArray(), filter.Name); var copyContext = GetFilterProvider(_filterContextParser.Serialize(filter.FilterProvider.GetFilter())); - var vm = new FilterViewModel(copyName, copyContext); + var copyMaterialsContext = GetMaterialsFilterProvider( + _filterContextParser.Serialize(filter.MaterialsFilterProvider.GetFilter())); + var vm = new FilterViewModel(copyName, copyContext, copyMaterialsContext, _filterContextParser); vm.PropertyChanged += OnFilterChanged; Filters.Add(vm); SelectedFilter = vm; @@ -144,7 +162,9 @@ private void CopyFilter(FilterViewModel filter) { } private bool CanCopyFilter(FilterViewModel filter) { - return filter is not null && filter.FilterProvider.CanGetFilter(out _); + return filter is not null + && filter.FilterProvider.CanGetFilter(out _) + && filter.MaterialsFilterProvider.CanGetFilter(out _); } private void RemoveFilters(IList items) { @@ -166,7 +186,7 @@ private bool CanRemoveFilters(IList items) { private void Save() { _filtersRepo.SetFilters(GetFilterModels(Filters)); foreach(var vm in Filters) { - vm.Modified = false; + vm.AcceptChanges(); } FiltersModified = false; @@ -186,7 +206,18 @@ private void Export() { private bool CanSave() { foreach(var vm in Filters) { if(!vm.FilterProvider.CanGetFilter(out var errors)) { - ErrorText = $"{vm.Name}: {errors.FirstOrDefault()?.Message}"; + ErrorText = $"{vm.Name}: " + + _localization.GetLocalizedString( + "FiltrationPage.Error.ElementsFilter", + errors.FirstOrDefault()?.Message ?? string.Empty); + return false; + } + + if(!vm.MaterialsFilterProvider.CanGetFilter(out var materialsErrors)) { + ErrorText = $"{vm.Name}: " + + _localization.GetLocalizedString( + "FiltrationPage.Error.MaterialsFilter", + materialsErrors.FirstOrDefault()?.Message ?? string.Empty); return false; } } @@ -198,7 +229,8 @@ private bool CanSave() { private Filter[] GetFilterModels(IEnumerable filterViewModels) { return filterViewModels.Select(f => new Filter() { Name = f.Name, - FilterContext = _filterContextParser.Serialize(f.FilterProvider.GetFilter()) + FilterContext = _filterContextParser.Serialize(f.FilterProvider.GetFilter()), + MaterialsFilterContext = _filterContextParser.Serialize(f.MaterialsFilterProvider.GetFilter()) }) .ToArray(); } @@ -249,7 +281,11 @@ private void OnFilterChanged(object sender, PropertyChangedEventArgs e) { } private FilterViewModel GetFilterViewModel(Filter filter) { - return new FilterViewModel(filter.Name, GetFilterProvider(filter.FilterContext)) { Modified = false }; + return new FilterViewModel( + filter.Name, + GetFilterProvider(filter.FilterContext), + GetMaterialsFilterProvider(filter.MaterialsFilterContext), + _filterContextParser) { Modified = false }; } private ILogicalFilterProvider GetFilterProvider(string serializedContext = "") { @@ -264,4 +300,23 @@ private ILogicalFilterProvider GetFilterProvider(string serializedContext = "") return _filterProviderFactory.Create(_dataProvider); } } + + /// + /// Создает провайдер фильтра по параметрам материалов элементов. + /// + /// Сериализованный контекст фильтра. + /// + /// Если контекст не задан, либо его не удалось прочитать, создается провайдер с пустым набором правил. + /// + private ILogicalFilterProvider GetMaterialsFilterProvider(string serializedContext = "") { + if(!string.IsNullOrWhiteSpace(serializedContext) + && _filterContextParser.TryParse(serializedContext, out var context)) { + return _filterProviderFactory.Create(_materialsDataProvider, context!); + } + + return _filterProviderFactory.Create( + _materialsDataProvider, + _filterFactory.CreateAndFilter(), + [BuiltInCategory.OST_Materials]); + } } diff --git a/src/RevitParamsChecker/Views/Filtration/FiltrationPage.xaml b/src/RevitParamsChecker/Views/Filtration/FiltrationPage.xaml index ddc3c9587..4042dfdf3 100644 --- a/src/RevitParamsChecker/Views/Filtration/FiltrationPage.xaml +++ b/src/RevitParamsChecker/Views/Filtration/FiltrationPage.xaml @@ -79,6 +79,21 @@ Grid.Row="1" Grid.Column="2" LanguageService="{Binding LanguageService}" - LogicalFilterProvider="{Binding SelectedFilter.FilterProvider}" /> + LocalizationService="{Binding LocalizationService}" + LogicalFilterProvider="{Binding SelectedFilter.FilterProvider}"> + + + + + \ No newline at end of file diff --git a/src/RevitParamsChecker/assets/Localization/Language.en-US.xaml b/src/RevitParamsChecker/assets/Localization/Language.en-US.xaml index e4905b430..397b58004 100644 --- a/src/RevitParamsChecker/assets/Localization/Language.en-US.xaml +++ b/src/RevitParamsChecker/assets/Localization/Language.en-US.xaml @@ -149,6 +149,14 @@ Previously selected checks are missing: x:Key="FiltrationPage.SaveFileDefaultName">Search Sets Configuration Failed to load search sets + Element materials parameters filter criteria + Element parameters filter: {0} + Element materials parameters filter: {0} + Element parameters filter criteria Load diff --git a/src/RevitParamsChecker/assets/Localization/Language.ru-RU.xaml b/src/RevitParamsChecker/assets/Localization/Language.ru-RU.xaml index 257b5d3b9..21ade2d2f 100644 --- a/src/RevitParamsChecker/assets/Localization/Language.ru-RU.xaml +++ b/src/RevitParamsChecker/assets/Localization/Language.ru-RU.xaml @@ -149,6 +149,14 @@ x:Key="FiltrationPage.SaveFileDefaultName">Настройка поисковых наборов Не удалось загрузить поисковые наборы + Критерии фильтрации по параметрам материалов элементов + Фильтр по параметрам элементов: {0} + Фильтр по параметрам материалов элементов: {0} + Критерии фильтрации по параметрам элементов Загрузить