From 0e327f10e0028aae967054b6f58d29e9d2a5a6ea Mon Sep 17 00:00:00 2001 From: Mankaev_r Date: Tue, 26 May 2026 17:41:36 +0300 Subject: [PATCH 01/11] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D0=BF=D1=80=D0=B5=D0=B6?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=20=D0=BD=D0=B5=D0=BE?= =?UTF-8?q?=D0=B1=D1=85=D0=BE=D0=B4=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/MainViewModel.cs | 18 ++++++++++ src/RevitUnmodelingMep/Views/MainWindow.xaml | 35 ++++++++++++++++--- .../assets/Localization/Language.en-US.xaml | 4 +++ .../assets/Localization/Language.ru-RU.xaml | 4 +++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/RevitUnmodelingMep/ViewModels/MainViewModel.cs b/src/RevitUnmodelingMep/ViewModels/MainViewModel.cs index 2c58d7bac..abea084f1 100644 --- a/src/RevitUnmodelingMep/ViewModels/MainViewModel.cs +++ b/src/RevitUnmodelingMep/ViewModels/MainViewModel.cs @@ -61,6 +61,7 @@ public MainViewModel( RemoveConsumableTypeCommand = RelayCommand.Create(RemoveConsumableType, CanRemoveConsumableType); ImportConfigsCommand = RelayCommand.Create(ImportConfigs); ExportConfigsCommand = RelayCommand.Create(ExportConfigs); + RefreshConfigsCommand = RelayCommand.Create(RefreshConfigs); ResetConfigsCommand = RelayCommand.Create(ResetConfigs); ConsumableTypes = new ObservableCollection(); @@ -83,6 +84,8 @@ public MainViewModel( public ICommand ExportConfigsCommand { get; } + public ICommand RefreshConfigsCommand { get; } + public ICommand ResetConfigsCommand { get; } public HintPanelViewModel Hint { get; } @@ -234,6 +237,21 @@ private void ResetConfigs(Window owner) { LoadConsumableTypesFromSettings(defaults); } + private void RefreshConfigs() { + string title = _localizationService.GetLocalizedString("MainWindow.Title"); + string confirmMessage = _localizationService.GetLocalizedString("MainWindow.RefreshConfirmMessage"); + if(MessageBoxService.Show( + confirmMessage, + title, + MessageBoxButton.OKCancel, + MessageBoxImage.Question) != MessageBoxResult.OK) { + return; + } + + string placeholderMessage = _localizationService.GetLocalizedString("MainWindow.RefreshPlaceholderMessage"); + MessageBoxService.Show(placeholderMessage, title, MessageBoxButton.OK, MessageBoxImage.Information); + } + private void ImportConfigs() { _openFileDialogService.Filter = "JSON files | *.json"; if(!_openFileDialogService.ShowDialog()) { diff --git a/src/RevitUnmodelingMep/Views/MainWindow.xaml b/src/RevitUnmodelingMep/Views/MainWindow.xaml index e18ff6ce7..aa3a25025 100644 --- a/src/RevitUnmodelingMep/Views/MainWindow.xaml +++ b/src/RevitUnmodelingMep/Views/MainWindow.xaml @@ -45,6 +45,10 @@ + + + + From e747c34392f301e85a5203473e65435599bceb3c Mon Sep 17 00:00:00 2001 From: Mankaev_r Date: Thu, 28 May 2026 12:04:46 +0300 Subject: [PATCH 03/11] =?UTF-8?q?MainViewModel.cs=20=D0=B4=D0=B5=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BF=D0=BE=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=8B=20=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D1=8F=D1=8E=D1=89=D0=B8=D0=B5=20=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=BC=20=D0=BD=D0=B0=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B8=20=D0=9C=D0=95?= =?UTF-8?q?=D0=A0-=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D1=8F?= =?UTF-8?q?=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена возможность не загружать из шаблона новые расходники --- .../ViewModels/CategoryAssignmentBuilder.cs | 130 ++++++++ .../ViewModels/CategoryOptionsProvider.cs | 98 ++++++ .../ViewModels/ConsumableTemplateManager.cs | 24 +- .../ViewModels/MainViewModel.cs | 280 ++++++++---------- .../assets/Localization/Language.en-US.xaml | 3 +- .../assets/Localization/Language.ru-RU.xaml | 3 +- 6 files changed, 372 insertions(+), 166 deletions(-) create mode 100644 src/RevitUnmodelingMep/ViewModels/CategoryAssignmentBuilder.cs create mode 100644 src/RevitUnmodelingMep/ViewModels/CategoryOptionsProvider.cs diff --git a/src/RevitUnmodelingMep/ViewModels/CategoryAssignmentBuilder.cs b/src/RevitUnmodelingMep/ViewModels/CategoryAssignmentBuilder.cs new file mode 100644 index 000000000..e137f45e9 --- /dev/null +++ b/src/RevitUnmodelingMep/ViewModels/CategoryAssignmentBuilder.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +using Autodesk.Revit.DB; + +using RevitUnmodelingMep.Models; + +namespace RevitUnmodelingMep.ViewModels; + +internal sealed class CategoryAssignmentBuilder { + private readonly RevitRepository _revitRepository; + + public CategoryAssignmentBuilder(RevitRepository revitRepository) { + _revitRepository = revitRepository; + } + + /// + /// Строит дерево назначений: категории Revit, типы систем и доступные для них конфигурации расходников. + /// + public ObservableCollection Build( + IReadOnlyList categoryOptions, + IEnumerable consumableTypes, + bool onlyPlacedInProject, + Func resolveCategoryId) { + List categories = new List { + BuiltInCategory.OST_PipeCurves, + BuiltInCategory.OST_DuctCurves, + BuiltInCategory.OST_PipeInsulations, + BuiltInCategory.OST_DuctInsulations, + BuiltInCategory.OST_DuctSystem, + BuiltInCategory.OST_PipingSystem + }; + + var assignments = new List(); + + foreach(BuiltInCategory builtInCategory in categories) { + CategoryOption option = categoryOptions.FirstOrDefault(o => o.BuiltInCategory == builtInCategory); + int optionCategoryId = option?.Id ?? (int) builtInCategory; + string categoryName = option?.Name ?? builtInCategory.ToString(); + + List types = CollectionGenerator.GetElementTypesByCategory(_revitRepository.Doc, builtInCategory) + ?? new List(); + if(types.Count == 0) { + continue; + } + + List configsForCategory = consumableTypes? + .Where(c => resolveCategoryId(c) == optionCategoryId) + .OrderBy(c => c?.ConsumableTypeName ?? string.Empty, StringComparer.CurrentCultureIgnoreCase) + .ToList() ?? new List(); + + HashSet placedTypeIds = onlyPlacedInProject ? GetPlacedTypeIds(builtInCategory) : null; + + ObservableCollection systemTypes = + new ObservableCollection( + types + .OfType() + .Where(type => placedTypeIds == null || placedTypeIds.Contains(GetElementIdValue(type.Id))) + .OrderBy(type => type?.Name ?? string.Empty, StringComparer.CurrentCultureIgnoreCase) + .Select(type => CreateSystemTypeItem(type, configsForCategory))); + + if(systemTypes.Count == 0) { + continue; + } + + assignments.Add(new CategoryAssignmentItem { + Name = categoryName, + Category = builtInCategory, + SystemTypes = systemTypes + }); + } + + return new ObservableCollection( + assignments.OrderBy(a => a?.Name ?? string.Empty, StringComparer.CurrentCultureIgnoreCase)); + } + + /// + /// Возвращает идентификаторы типов, которые реально размещены в проекте для указанной категории. + /// + private HashSet GetPlacedTypeIds(BuiltInCategory builtInCategory) { + var result = new HashSet(); + + var collector = new FilteredElementCollector(_revitRepository.Doc) + .OfCategory(builtInCategory) + .WhereElementIsNotElementType(); + + foreach(Element element in collector) { + ElementId typeId = element.GetTypeId(); + if(typeId == null || typeId == ElementId.InvalidElementId) { + continue; + } + + result.Add(GetElementIdValue(typeId)); + } + + return result; + } + + /// + /// Создает элемент типа системы с назначениями всех расходников, подходящих для его категории. + /// + private static SystemTypeItem CreateSystemTypeItem(ElementType elementType, List configs) { + int typeId = GetElementIdValue(elementType.Id); + + ObservableCollection configAssignments = + new ObservableCollection( + configs.Select(config => new ConfigAssignmentItem(config, typeId))); + + return new SystemTypeItem { + Name = elementType.Name, + Id = typeId, + Configs = configAssignments + }; + } + + /// + /// Возвращает числовое значение ElementId с учетом различий API Revit до и после 2024 версии. + /// + private static int GetElementIdValue(ElementId elementId) { + long value; +#if REVIT_2024_OR_GREATER + value = elementId?.Value ?? 0; +#else + value = elementId?.IntegerValue ?? 0; +#endif + return unchecked((int) value); + } +} diff --git a/src/RevitUnmodelingMep/ViewModels/CategoryOptionsProvider.cs b/src/RevitUnmodelingMep/ViewModels/CategoryOptionsProvider.cs new file mode 100644 index 000000000..ffdcf815a --- /dev/null +++ b/src/RevitUnmodelingMep/ViewModels/CategoryOptionsProvider.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; + +using dosymep.SimpleServices; + +namespace RevitUnmodelingMep.ViewModels; + +internal sealed class CategoryOptionsProvider { + private readonly Document _document; + private readonly ILocalizationService _localizationService; + + /// + /// Создает поставщик доступных MEP-категорий для настроек расходников. + /// + public CategoryOptionsProvider(Document document, ILocalizationService localizationService) { + _document = document; + _localizationService = localizationService; + } + + /// + /// Создает справочник категорий, доступных для выбора в настройках расходников. + /// + public IReadOnlyList CreateCategoryOptions() { + return new List { + CreateCategoryOption( + _localizationService.GetLocalizedString("MainViewModel.DuctsName"), + BuiltInCategory.OST_DuctCurves), + CreateCategoryOption( + _localizationService.GetLocalizedString("MainViewModel.PipesName"), + BuiltInCategory.OST_PipeCurves), + CreateCategoryOption( + _localizationService.GetLocalizedString("MainViewModel.PipeInsName"), + BuiltInCategory.OST_PipeInsulations), + CreateCategoryOption( + _localizationService.GetLocalizedString("MainViewModel.DuctInsName"), + BuiltInCategory.OST_DuctInsulations), + CreateCategoryOption( + _localizationService.GetLocalizedString("MainViewModel.DuctSysName"), + BuiltInCategory.OST_DuctSystem), + CreateCategoryOption( + _localizationService.GetLocalizedString("MainViewModel.PipeSysName"), + BuiltInCategory.OST_PipingSystem) + }; + } + + /// + /// Преобразует сохраненное значение категории в один из доступных вариантов выбора. + /// + public CategoryOption ResolveCategoryOption( + IReadOnlyList categoryOptions, + string categoryValue) { + if(string.IsNullOrWhiteSpace(categoryValue)) { + return categoryOptions.FirstOrDefault(); + } + + if(int.TryParse(categoryValue, out int id)) { + return categoryOptions.FirstOrDefault(o => o.Id == id); + } + + string trimmed = categoryValue.Trim(); + if(trimmed.StartsWith("BuiltInCategory.", StringComparison.OrdinalIgnoreCase)) { + string enumName = trimmed.Substring("BuiltInCategory.".Length); + CategoryOption byEnumName = categoryOptions.FirstOrDefault(o => + string.Equals(o.BuiltInCategory.ToString(), enumName, StringComparison.OrdinalIgnoreCase)); + if(byEnumName != null) { + return byEnumName; + } + } + + return categoryOptions.FirstOrDefault(o => + string.Equals(o.Name, categoryValue, StringComparison.OrdinalIgnoreCase)) + ?? categoryOptions.FirstOrDefault(); + } + + /// + /// Создает один вариант категории с локализованным именем, BuiltInCategory и фактическим id категории в документе. + /// + private CategoryOption CreateCategoryOption(string name, BuiltInCategory builtInCategory) { + Category category = Category.GetCategory(_document, builtInCategory); + + long idValue; +#if REVIT_2024_OR_GREATER + idValue = category?.Id.Value ?? (long) (int) builtInCategory; +#else + idValue = category?.Id.IntegerValue ?? (int) builtInCategory; +#endif + int id = unchecked((int) idValue); + + return new CategoryOption { + Name = name, + BuiltInCategory = builtInCategory, + Id = id + }; + } +} diff --git a/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs b/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs index ff9ff1ca6..bdbc1641b 100644 --- a/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs +++ b/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs @@ -54,7 +54,14 @@ public void Attach(ObservableCollection items) { /// /// Приводит существующие шаблонные расходники к значениям из шаблона и добавляет отсутствующие шаблонные расходники. /// - public void ApplyTemplatesToItems() { + /// + /// Проверяет, есть ли в шаблоне расходники, которые отсутствуют в текущей коллекции. + /// + public bool HasMissingTemplateItems() { + return _items != null && _templateConfigs.Values.Any(template => !ContainsTemplateItem(template)); + } + + public void ApplyTemplatesToItems(bool addMissingItems) { if(_items == null) { return; } @@ -77,6 +84,10 @@ public void ApplyTemplatesToItems() { continue; } + if(!addMissingItems) { + continue; + } + string configKey = GetUniqueConfigKey(template.ConfigKey, usedConfigKeys); usedConfigKeys.Add(configKey); @@ -88,6 +99,17 @@ public void ApplyTemplatesToItems() { RefreshWarnings(); } + /// + /// Проверяет, есть ли в текущей коллекции расходник с именем указанного шаблонного расходника. + /// + private bool ContainsTemplateItem(TemplateConsumable template) { + return _items.Any(item => + string.Equals( + item?.ConsumableTypeName, + template.Config.ConfigName, + StringComparison.CurrentCultureIgnoreCase)); + } + /// /// Снимает подписки с ранее подключенной коллекции и ее элементов. /// diff --git a/src/RevitUnmodelingMep/ViewModels/MainViewModel.cs b/src/RevitUnmodelingMep/ViewModels/MainViewModel.cs index 7da0c08ad..aecde1585 100644 --- a/src/RevitUnmodelingMep/ViewModels/MainViewModel.cs +++ b/src/RevitUnmodelingMep/ViewModels/MainViewModel.cs @@ -12,7 +12,6 @@ using dosymep.WPF.ViewModels; using pyRevitLabs.Json; -using Autodesk.Revit.DB; using RevitUnmodelingMep.Models; @@ -28,6 +27,8 @@ internal class MainViewModel : BaseViewModel { public IMessageBoxService MessageBoxService { get; } private readonly IConfigSerializer _configSerializer; private readonly ConsumableTemplateManager _consumableTemplateManager; + private readonly CategoryOptionsProvider _categoryOptionsProvider; + private readonly CategoryAssignmentBuilder _categoryAssignmentBuilder; private string _errorText; private string _saveProperty; @@ -37,8 +38,7 @@ internal class MainViewModel : BaseViewModel { private int _lastConfigIndex; private readonly IReadOnlyList _categoryOptions; private bool _isViewLoaded; - - + public MainViewModel( PluginConfig pluginConfig, RevitRepository revitRepository, @@ -55,7 +55,9 @@ public MainViewModel( _saveFileDialogService = saveFileDialogService; MessageBoxService = messageBoxService; _configSerializer = configSerializer; - _categoryOptions = CreateCategoryOptions(); + _categoryOptionsProvider = new CategoryOptionsProvider(_revitRepository.Document, _localizationService); + _categoryAssignmentBuilder = new CategoryAssignmentBuilder(_revitRepository); + _categoryOptions = _categoryOptionsProvider.CreateCategoryOptions(); _consumableTemplateManager = new ConsumableTemplateManager( _revitRepository.VisSettingsStorage, ResolveCategoryOption); @@ -98,17 +100,26 @@ public MainViewModel( public ISaveFileDialogService SaveFileDialogService => _saveFileDialogService; + /// + /// Хранит текст ошибки валидации, который показывается в нижней части окна. + /// public string ErrorText { get => _errorText; set => RaiseAndSetIfChanged(ref _errorText, value); } + /// + /// Хранит имя параметра, в который сохраняется результат работы настроек. + /// public string SaveProperty { get => _saveProperty; set => RaiseAndSetIfChanged(ref _saveProperty, value); } + /// + /// Хранит редактируемый список расходников и переподключает к нему менеджер шаблонов при замене коллекции. + /// public ObservableCollection ConsumableTypes { get => _consumableTypes; set { @@ -117,6 +128,9 @@ public ObservableCollection ConsumableTypes { } } + /// + /// Хранит построенные группы назначений расходников на типы элементов Revit. + /// public ObservableCollection CategoryAssignments { get => _categoryAssignments; set => RaiseAndSetIfChanged(ref _categoryAssignments, value); @@ -125,6 +139,9 @@ public ObservableCollection CategoryAssignments { public IReadOnlyList CategoryOptions => _categoryOptions; public bool IsViewLoaded => _isViewLoaded; + /// + /// Управляет режимом отображения только размещенных в проекте типов и пересобирает списки после изменения. + /// public bool OnlyPlacedInProject { get => _onlyPlacedInProject; set { @@ -141,15 +158,24 @@ public bool OnlyPlacedInProject { } + /// + /// Загружает данные окна после события Loaded. + /// private void LoadView() { LoadConfig(); } + /// + /// Сохраняет настройки после подтверждения окна. + /// private void AcceptView() { SaveConfig(); } + /// + /// Проверяет корректность формул и обязательных настроек перед сохранением окна. + /// private bool CanAcceptView() { bool isValid = FormulaValidator.ValidateFormulas( ConsumableTypes, @@ -164,6 +190,9 @@ private bool CanAcceptView() { } + /// + /// Загружает проектные настройки, настройки расходников и начальные списки назначений. + /// private void LoadConfig() { RevitSettings setting = _pluginConfig.GetSettings(_revitRepository.Document); @@ -174,6 +203,9 @@ private void LoadConfig() { _isViewLoaded = true; } + /// + /// Сохраняет параметр результата, настройки расходников и проектный конфиг плагина. + /// private void SaveConfig() { RevitSettings setting = _pluginConfig.GetSettings(_revitRepository.Document) ?? _pluginConfig.AddSettings(_revitRepository.Document); @@ -183,6 +215,9 @@ private void SaveConfig() { _pluginConfig.SaveProjectConfig(); } + /// + /// Обновляет списки назначений после изменений расходников, когда представление уже загружено. + /// public void RefreshAssignmentsFromConsumableTypes() { if(!_isViewLoaded) { return; @@ -191,6 +226,9 @@ public void RefreshAssignmentsFromConsumableTypes() { UpdateTypesLists(); } + /// + /// Создает новый пользовательский расходник с новым ключом конфигурации и категорией по умолчанию. + /// private void AddConsumableType() { int index = ConsumableTypes.Count + 1; string configKey = GetNextConfigKey(); @@ -208,10 +246,16 @@ private void AddConsumableType() { UpdateTypesLists(); } + /// + /// Разрешает удаление расходника, если в коллекции есть хотя бы один элемент. + /// private bool CanRemoveConsumableType(ConsumableTypeItem item) { return ConsumableTypes?.Count > 0; } + /// + /// Удаляет расходник из коллекции и пересобирает списки назначений. + /// private void RemoveConsumableType(ConsumableTypeItem item) { if(ConsumableTypes == null || ConsumableTypes.Count == 0 || item == null) { return; @@ -232,6 +276,9 @@ private void RemoveConsumableType(ConsumableTypeItem item) { UpdateTypesLists(); } + /// + /// После подтверждения заменяет текущие настройки расходников настройками из файла шаблона. + /// private void ResetConfigs(Window owner) { string message = _localizationService.GetLocalizedString("MainWindow.ResetConfirmMessage"); string title = _localizationService.GetLocalizedString("MainWindow.Title"); @@ -245,6 +292,9 @@ private void ResetConfigs(Window owner) { LoadConsumableTypesFromSettings(defaults); } + /// + /// После подтверждения приводит шаблонные расходники к значениям шаблона и опционально добавляет отсутствующие. + /// private void RefreshConfigs() { string title = _localizationService.GetLocalizedString("MainWindow.Title"); string confirmMessage = _localizationService.GetLocalizedString("MainWindow.RefreshConfirmMessage"); @@ -256,11 +306,25 @@ private void RefreshConfigs() { return; } - _consumableTemplateManager.ApplyTemplatesToItems(); + bool addMissingTemplateItems = true; + if(_consumableTemplateManager.HasMissingTemplateItems()) { + string addMissingMessage = _localizationService.GetLocalizedString( + "MainWindow.AddMissingTemplateConsumablesConfirmMessage"); + addMissingTemplateItems = MessageBoxService.Show( + addMissingMessage, + title, + MessageBoxButton.OKCancel, + MessageBoxImage.Question) == MessageBoxResult.OK; + } + + _consumableTemplateManager.ApplyTemplatesToItems(addMissingTemplateItems); SyncLastConfigIndex(); UpdateTypesLists(); } + /// + /// Импортирует настройки расходников из JSON-файла и заменяет ими текущую коллекцию. + /// private void ImportConfigs() { _openFileDialogService.Filter = "JSON files | *.json"; if(!_openFileDialogService.ShowDialog()) { @@ -282,6 +346,9 @@ private void ImportConfigs() { } } + /// + /// Экспортирует текущие настройки расходников и сопутствующие настройки в JSON-файл. + /// private void ExportConfigs() { _saveFileDialogService.AddExtension = true; _saveFileDialogService.Filter = "JSON files | *.json"; @@ -305,6 +372,9 @@ private void ExportConfigs() { _configSerializer.Serialize(exported)); } + /// + /// Применяет документ настроек к модели окна и заменяет коллекцию расходников отсортированными элементами. + /// private void LoadConsumableTypesFromSettings(UnmodelingSettingsDocument settings) { settings ??= new UnmodelingSettingsDocument(); ApplySettings(settings); @@ -319,6 +389,9 @@ private void LoadConsumableTypesFromSettings(UnmodelingSettingsDocument settings UpdateTypesLists(); } + /// + /// Загружает сохраненные в проекте настройки расходников из хранилища VISSettings. + /// private void LoadUnmodelingConfigs() { UnmodelingSettingsDocument settings = _revitRepository.VisSettingsStorage.GetUnmodelingSettings(); ApplySettings(settings); @@ -332,6 +405,9 @@ private void LoadUnmodelingConfigs() { ConsumableTypes = CreateSortedConsumableTypes(consumableTypes); } + /// + /// Собирает текущие настройки расходников и сохраняет их в хранилище VISSettings. + /// private void SaveUnmodelingConfigs() { UnmodelingSettingsDocument settings = _revitRepository.VisSettingsStorage.GetUnmodelingSettings(); settings ??= new UnmodelingSettingsDocument(); @@ -340,6 +416,9 @@ private void SaveUnmodelingConfigs() { _revitRepository.VisSettingsStorage.SaveUnmodelingSettings(settings); } + /// + /// Преобразует редактируемые расходники окна в словарь конфигурации для хранения и экспорта. + /// private Dictionary BuildUnmodelingConfigs() { var configs = new Dictionary(); if(ConsumableTypes == null) { @@ -357,6 +436,9 @@ private Dictionary BuildUnmodelingConfigs() { return configs; } + /// + /// Создает новую коллекцию расходников, отсортированную по ключу конфигурации. + /// private static ObservableCollection CreateSortedConsumableTypes( IEnumerable consumableTypes) { return new ObservableCollection( @@ -364,23 +446,35 @@ private static ObservableCollection CreateSortedConsumableTy .OrderBy(item => item?.ConfigKey ?? string.Empty, StringComparer.CurrentCultureIgnoreCase)); } + /// + /// Собирает дополнительные настройки раздела немоделируемых элементов. + /// private UnmodelingSettingsOptions BuildUnmodelingSettings() { return new UnmodelingSettingsOptions { OnlyProjectInstances = OnlyPlacedInProject }; } + /// + /// Применяет дополнительные настройки документа к состоянию окна. + /// private void ApplySettings(UnmodelingSettingsDocument settings) { if(settings?.UnmodelingSettings != null) { OnlyPlacedInProject = settings.UnmodelingSettings.OnlyProjectInstances; } } + /// + /// Возвращает следующий ключ конфигурации вида config_NNN и продвигает внутренний счетчик. + /// private string GetNextConfigKey() { _lastConfigIndex++; return $"config_{_lastConfigIndex:000}"; } + /// + /// Синхронизирует внутренний счетчик ключей после массового добавления или замены расходников. + /// private void SyncLastConfigIndex() { int lastConfigIndex = 0; foreach(ConsumableTypeItem item in ConsumableTypes ?? Enumerable.Empty()) { @@ -393,102 +487,20 @@ private void SyncLastConfigIndex() { _lastConfigIndex = lastConfigIndex; } + /// + /// Строит дерево назначений: категории Revit, типы систем и доступные для них конфигурации расходников. + /// private void UpdateTypesLists() { - List categories = new List { - BuiltInCategory.OST_PipeCurves, - BuiltInCategory.OST_DuctCurves, - BuiltInCategory.OST_PipeInsulations, - BuiltInCategory.OST_DuctInsulations, - BuiltInCategory.OST_DuctSystem, - BuiltInCategory.OST_PipingSystem - }; - - var assignments = new List(); - - foreach(BuiltInCategory builtInCategory in categories) { - CategoryOption option = CategoryOptions.FirstOrDefault(o => o.BuiltInCategory == builtInCategory); - int optionCategoryId = option?.Id ?? (int) builtInCategory; - string categoryName = option?.Name ?? builtInCategory.ToString(); - - List types = CollectionGenerator.GetElementTypesByCategory(_revitRepository.Doc, builtInCategory) - ?? new List(); - if(types.Count == 0) { - continue; - } - - List configsForCategory = ConsumableTypes? - .Where(c => TryGetCategoryId(c, out int cid) && cid == optionCategoryId) - .OrderBy(c => c?.ConsumableTypeName ?? string.Empty, StringComparer.CurrentCultureIgnoreCase) - .ToList() ?? new List(); - - HashSet placedTypeIds = OnlyPlacedInProject ? GetPlacedTypeIds(builtInCategory) : null; - - ObservableCollection systemTypes = - new ObservableCollection( - types - .OfType() - .Where(type => placedTypeIds == null || placedTypeIds.Contains(GetElementIdValue(type.Id))) - .OrderBy(type => type?.Name ?? string.Empty, StringComparer.CurrentCultureIgnoreCase) - .Select(type => CreateSystemTypeItem(type, configsForCategory))); - - if(systemTypes.Count == 0) { - continue; - } - - assignments.Add(new CategoryAssignmentItem { - Name = categoryName, - Category = builtInCategory, - SystemTypes = systemTypes - }); - } - - CategoryAssignments = new ObservableCollection( - assignments.OrderBy(a => a?.Name ?? string.Empty, StringComparer.CurrentCultureIgnoreCase)); - } - - private HashSet GetPlacedTypeIds(BuiltInCategory builtInCategory) { - var result = new HashSet(); - - var collector = new FilteredElementCollector(_revitRepository.Doc) - .OfCategory(builtInCategory) - .WhereElementIsNotElementType(); - - foreach(Element element in collector) { - ElementId typeId = element.GetTypeId(); - if(typeId == null || typeId == ElementId.InvalidElementId) { - continue; - } - - result.Add(GetElementIdValue(typeId)); - } - - return result; - } - - private SystemTypeItem CreateSystemTypeItem(ElementType elementType, List configs) { - int typeId = GetElementIdValue(elementType.Id); - - ObservableCollection configAssignments = - new ObservableCollection( - configs.Select(config => new ConfigAssignmentItem(config, typeId))); - - return new SystemTypeItem { - Name = elementType.Name, - Id = typeId, - Configs = configAssignments - }; - } - - private static int GetElementIdValue(ElementId elementId) { - long value; -#if REVIT_2024_OR_GREATER - value = elementId?.Value ?? 0; -#else - value = elementId?.IntegerValue ?? 0; -#endif - return unchecked((int) value); + CategoryAssignments = _categoryAssignmentBuilder.Build( + CategoryOptions, + ConsumableTypes, + OnlyPlacedInProject, + ResolveCategoryId); } + /// + /// Пытается получить числовой идентификатор категории из выбранной категории или сохраненного строкового значения. + /// private static bool TryGetCategoryId(ConsumableTypeItem item, out int categoryId) { if(item?.SelectedCategory != null) { categoryId = item.SelectedCategory.Id; @@ -504,75 +516,17 @@ private static bool TryGetCategoryId(ConsumableTypeItem item, out int categoryId return false; } + /// + /// Возвращает nullable-идентификатор категории расходника для валидатора формул. + /// private int? ResolveCategoryId(ConsumableTypeItem item) { return TryGetCategoryId(item, out int cid) ? cid : (int?) null; } - private IReadOnlyList CreateCategoryOptions() { - List options = new List { - - CreateCategoryOption( - _localizationService.GetLocalizedString("MainViewModel.DuctsName"), - BuiltInCategory.OST_DuctCurves), - CreateCategoryOption( - _localizationService.GetLocalizedString("MainViewModel.PipesName"), - BuiltInCategory.OST_PipeCurves), - CreateCategoryOption( - _localizationService.GetLocalizedString("MainViewModel.PipeInsName"), - BuiltInCategory.OST_PipeInsulations), - CreateCategoryOption( - _localizationService.GetLocalizedString("MainViewModel.DuctInsName"), - BuiltInCategory.OST_DuctInsulations), - CreateCategoryOption( - _localizationService.GetLocalizedString("MainViewModel.DuctSysName"), - BuiltInCategory.OST_DuctSystem), - CreateCategoryOption( - _localizationService.GetLocalizedString("MainViewModel.PipeSysName"), - BuiltInCategory.OST_PipingSystem) - }; - - return options; - } - - private CategoryOption CreateCategoryOption(string name, BuiltInCategory builtInCategory) { - Category category = Category.GetCategory(_revitRepository.Document, builtInCategory); - - long idValue; -#if REVIT_2024_OR_GREATER - idValue = category?.Id.Value ?? (long) (int) builtInCategory; -#else - idValue = category?.Id.IntegerValue ?? (int) builtInCategory; -#endif - int id = unchecked((int) idValue); - - return new CategoryOption { - Name = name, - BuiltInCategory = builtInCategory, - Id = id - }; - } - + /// + /// Преобразует сохраненное значение категории в один из доступных вариантов выбора. + /// private CategoryOption ResolveCategoryOption(string categoryValue) { - if(string.IsNullOrWhiteSpace(categoryValue)) { - return CategoryOptions.FirstOrDefault(); - } - - if(int.TryParse(categoryValue, out int id)) { - return CategoryOptions.FirstOrDefault(o => o.Id == id); - } - - string trimmed = categoryValue.Trim(); - if(trimmed.StartsWith("BuiltInCategory.", StringComparison.OrdinalIgnoreCase)) { - string enumName = trimmed.Substring("BuiltInCategory.".Length); - CategoryOption byEnumName = CategoryOptions.FirstOrDefault(o => - string.Equals(o.BuiltInCategory.ToString(), enumName, StringComparison.OrdinalIgnoreCase)); - if(byEnumName != null) { - return byEnumName; - } - } - - return CategoryOptions.FirstOrDefault(o => - string.Equals(o.Name, categoryValue, StringComparison.OrdinalIgnoreCase)) - ?? CategoryOptions.FirstOrDefault(); + return _categoryOptionsProvider.ResolveCategoryOption(CategoryOptions, categoryValue); } } diff --git a/src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml b/src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml index 2d964a202..0d3b9c965 100644 --- a/src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml +++ b/src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml @@ -18,7 +18,8 @@ Export Refresh Reset - Refreshing will reset all template consumables to the default view and add new templates. Continue? + Refreshing will reset all template consumables to the default view. Continue? + The template contains consumable items that do not exist in the project. Add them? Refresh is not implemented yet. Are you sure you want to reset to default values? Does not match the template. Refresh or rename. diff --git a/src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml b/src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml index 2931a6d62..ad606a9d0 100644 --- a/src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml +++ b/src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml @@ -18,7 +18,8 @@ Экспорт Обновить Сброс - При обновлении все шаблонные расходники будут сброшены к виду по умолчанию, а новые шаблоны будут добавлены. Продолжить? + При обновлении все шаблонные расходники будут сброшены к виду по умолчанию. Продолжить? + В шаблоне обнаружены расходные элементы, не существующие в проекте. Добавить их? Обновление пока не реализовано. Вы уверены что хотите сбросить до значений по умолчанию? Не соответствует шаблону. Выполните обновление или переименуйте. From a9a0844b716a8fd78e0e83a59a1991f4492b2bc3 Mon Sep 17 00:00:00 2001 From: Mankaev_r Date: Thu, 28 May 2026 14:10:53 +0300 Subject: [PATCH 04/11] =?UTF-8?q?=D0=92=D1=8B=D1=87=D0=B8=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/ConsumableTemplateManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs b/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs index bdbc1641b..5ac469ad9 100644 --- a/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs +++ b/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs @@ -51,9 +51,6 @@ public void Attach(ObservableCollection items) { RefreshWarnings(); } - /// - /// Приводит существующие шаблонные расходники к значениям из шаблона и добавляет отсутствующие шаблонные расходники. - /// /// /// Проверяет, есть ли в шаблоне расходники, которые отсутствуют в текущей коллекции. /// @@ -61,6 +58,9 @@ public bool HasMissingTemplateItems() { return _items != null && _templateConfigs.Values.Any(template => !ContainsTemplateItem(template)); } + /// + /// Приводит существующие шаблонные расходники к значениям из шаблона и при необходимости добавляет отсутствующие. + /// public void ApplyTemplatesToItems(bool addMissingItems) { if(_items == null) { return; From 080709d783f6eac0c60ebf79a1a571ff25fae09c Mon Sep 17 00:00:00 2001 From: Mankaev_r Date: Thu, 28 May 2026 16:00:51 +0300 Subject: [PATCH 05/11] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/ConsumableTemplateManager.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs b/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs index 5ac469ad9..5aa59c35b 100644 --- a/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs +++ b/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs @@ -317,23 +317,35 @@ private string ResolveTemplateCategory(string category) { } /// - /// Сравнивает nullable-строки как пустые строки по правилам текущей культуры. + /// Сравнивает nullable-строки как пустые строки по правилам текущей культуры, + /// чтобы не показывать предупреждение о расхождении шаблона только из-за разницы между null и пустым значением. /// private static bool StringEquals(string left, string right) { return string.Equals(left ?? string.Empty, right ?? string.Empty, StringComparison.CurrentCulture); } + /// + /// Хранит шаблонный расходник вместе с исходным ключом конфигурации, + /// чтобы при добавлении недостающего расходника сохранить ключ из шаблона или проверить его на конфликт. + /// private sealed class TemplateConsumable { /// - /// Создает внутреннее представление шаблонного расходника вместе с его ключом конфигурации. + /// Создает внутреннее представление шаблонного расходника и подставляет пустую конфигурацию, + /// если из настроек пришло null-значение. /// public TemplateConsumable(string configKey, UnmodelingConfigItem config) { ConfigKey = configKey; Config = config ?? new UnmodelingConfigItem(); } + /// + /// Исходный ключ шаблона в словаре настроек, который используется при создании недостающего расходника. + /// public string ConfigKey { get; } + /// + /// Значения шаблона, с которыми сравнивается и синхронизируется редактируемый расходник. + /// public UnmodelingConfigItem Config { get; } } } From 8639b067e3ac038dd4bfa0b630745e8efd8ca0f9 Mon Sep 17 00:00:00 2001 From: Mankaev_r Date: Thu, 28 May 2026 16:31:13 +0300 Subject: [PATCH 06/11] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs b/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs index 5aa59c35b..e26ce0e7f 100644 --- a/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs +++ b/src/RevitUnmodelingMep/ViewModels/ConsumableTemplateManager.cs @@ -310,7 +310,7 @@ private static int GetMaxConfigIndex(IEnumerable configKeys) { } /// - /// Приводит значение категории шаблона к тому же формату идентификатора, который используют редактируемые элементы. + /// Возвращает идентификатор категории шаблона в том виде, в котором он хранится у редактируемого расходника. /// private string ResolveTemplateCategory(string category) { return _resolveCategoryOption?.Invoke(category)?.Id.ToString() ?? category ?? string.Empty; From 81164607c6bbb697093c05f5c66cd0372662443d Mon Sep 17 00:00:00 2001 From: Mankaev_r Date: Thu, 28 May 2026 16:36:45 +0300 Subject: [PATCH 07/11] =?UTF-8?q?=D0=92=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA?= =?UTF-8?q?=D0=B0=20"=D0=9D=D0=B0=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2"=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/RevitUnmodelingMep/Views/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RevitUnmodelingMep/Views/MainWindow.xaml b/src/RevitUnmodelingMep/Views/MainWindow.xaml index ee70506d9..babe2ced0 100644 --- a/src/RevitUnmodelingMep/Views/MainWindow.xaml +++ b/src/RevitUnmodelingMep/Views/MainWindow.xaml @@ -251,7 +251,7 @@ Style="{StaticResource ContentTabControlStyle}" Background="Transparent" BorderThickness="0" - SelectedIndex="0" + SelectedIndex="1" SelectionChanged="ContentTabs_OnSelectionChanged"> From e9e0546fdeb755aef0c94110393bde9b4be8e06b Mon Sep 17 00:00:00 2001 From: Mankaev_r Date: Thu, 28 May 2026 16:45:34 +0300 Subject: [PATCH 08/11] =?UTF-8?q?=D0=92=D1=8B=D1=87=D0=B8=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml | 1 - src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml b/src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml index 0d3b9c965..9d4333093 100644 --- a/src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml +++ b/src/RevitUnmodelingMep/assets/Localization/Language.en-US.xaml @@ -20,7 +20,6 @@ Reset Refreshing will reset all template consumables to the default view. Continue? The template contains consumable items that do not exist in the project. Add them? - Refresh is not implemented yet. Are you sure you want to reset to default values? Does not match the template. Refresh or rename. Only placed in project diff --git a/src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml b/src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml index ad606a9d0..8a56b4b12 100644 --- a/src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml +++ b/src/RevitUnmodelingMep/assets/Localization/Language.ru-RU.xaml @@ -20,9 +20,8 @@ Сброс При обновлении все шаблонные расходники будут сброшены к виду по умолчанию. Продолжить? В шаблоне обнаружены расходные элементы, не существующие в проекте. Добавить их? - Обновление пока не реализовано. Вы уверены что хотите сбросить до значений по умолчанию? - Не соответствует шаблону. Выполните обновление или переименуйте. + Не соответствует шаблону. Нажмите кнопку "Обновить" или переименуйте. Только размещенные в проекте Выбрать все From 1f2e6d1aac8fb7ae69ca6176c62219ffc3c5427c Mon Sep 17 00:00:00 2001 From: Mankaev_r Date: Thu, 28 May 2026 16:49:41 +0300 Subject: [PATCH 09/11] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D1=91=D0=BD=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B9=20=D1=81=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=20=D0=B4=D0=BB=D1=8F=20ToolTip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/RevitUnmodelingMep/Views/MainWindow.xaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/RevitUnmodelingMep/Views/MainWindow.xaml b/src/RevitUnmodelingMep/Views/MainWindow.xaml index babe2ced0..ae8b4f325 100644 --- a/src/RevitUnmodelingMep/Views/MainWindow.xaml +++ b/src/RevitUnmodelingMep/Views/MainWindow.xaml @@ -45,10 +45,6 @@ - -