diff --git a/.github/workflows/publish.RevitClassifierParameters.yml b/.github/workflows/publish.RevitClassifierParameters.yml new file mode 100644 index 000000000..925325e6c --- /dev/null +++ b/.github/workflows/publish.RevitClassifierParameters.yml @@ -0,0 +1,37 @@ +name: publish RevitClassifierParameters + +on: + workflow_dispatch: + pull_request: + types: [ closed, synchronize, review_requested ] + branches: [ main, master ] + paths: + - '**RevitClassifierParameters**.cs' + - '**RevitClassifierParameters**.xaml' + +env: + plugin-name: "RevitClassifierParameters" + +jobs: + build: + name: build + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # Install the .NET workload + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + # Add DevExrepress package source: https://nuget.devexpress.com/ + - name: Add DevExpress package source + run: dotnet nuget add source https://nuget.devexpress.com/api -n DXFeed -u DevExpress -p ${{ secrets.DEVEXPRESS_FEED_AUTHORIZATION_KEY }} + + - name: Run './build.cmd ' + run: ./build.cmd publish --profile ${{ env.plugin-name }} --pull-request-merged ${{ github.event.pull_request.merged }} --extensions-app-token ${{ secrets.EXTENSIONS_APP_TOKEN }} --revit-plugins-app-token ${{ secrets.REVIT_PLUGINS_APP_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.nuke/parameters.RevitClassifierParameters.json b/.nuke/parameters.RevitClassifierParameters.json new file mode 100644 index 000000000..ef735f922 --- /dev/null +++ b/.nuke/parameters.RevitClassifierParameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "./build.schema.json", + "Solution": "RevitPlugins.slnx", + "PluginName": "RevitClassifierParameters", + "PublishDirectory": "01.BIM.extension\\BIM.tab\\bin", + "RevitVersions": [ + "Rv2022", + "Rv2023", + "Rv2024" + ], + "IconUrl": "https://icons8.ru/icon/14624/библиотека", + "BundleName": "Параметры ВОР АР", + "BundleType": "InvokeButton", + "BundleOutput": "01.BIM.extension\\BIM.tab\\ВОР.panel\\Параметры ВОР.pulldown" +} diff --git a/RevitPlugins.slnx b/RevitPlugins.slnx index 03e5e8634..37f8c6906 100644 --- a/RevitPlugins.slnx +++ b/RevitPlugins.slnx @@ -64,6 +64,9 @@ + + + diff --git a/src/RevitClassifierParameters/MarkupExtensions/EnumToLocaleExtension.cs b/src/RevitClassifierParameters/MarkupExtensions/EnumToLocaleExtension.cs new file mode 100644 index 000000000..cb00966c8 --- /dev/null +++ b/src/RevitClassifierParameters/MarkupExtensions/EnumToLocaleExtension.cs @@ -0,0 +1,51 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Markup; + +using dosymep.SimpleServices; +using dosymep.WpfCore; + +namespace RevitClassifierParameters.MarkupExtensions; + +internal sealed class EnumToLocaleExtension : MarkupExtension, IValueConverter { + private IHasLocalization _hasLocalization; + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { + if(_hasLocalization is null) { + return value; + } + + string enumName = $"{value?.GetType().Name}.{value}"; + return _hasLocalization.LocalizationService.GetLocalizedString(enumName); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { + return Binding.DoNothing; + } + + public override object ProvideValue(IServiceProvider serviceProvider) { + _hasLocalization = serviceProvider.GetRootObject(); + if(_hasLocalization is not null) { + return this; + } + + var frameworkElement = serviceProvider.GetRootObject(); + if(frameworkElement is not null) { + frameworkElement.Loaded += FrameworkElementOnLoaded; + } + + return this; + } + + private void FrameworkElementOnLoaded(object sender, RoutedEventArgs e) { + if(sender is IHasLocalization hasLocalization) { + _hasLocalization = hasLocalization; + } + + if(sender is FrameworkElement frameworkElement) { + frameworkElement.Loaded -= FrameworkElementOnLoaded; + } + } +} diff --git a/src/RevitClassifierParameters/Models/ExcelReaderBase.cs b/src/RevitClassifierParameters/Models/ExcelReaderBase.cs new file mode 100644 index 000000000..e5abf3ef9 --- /dev/null +++ b/src/RevitClassifierParameters/Models/ExcelReaderBase.cs @@ -0,0 +1,92 @@ +using System.IO; + +using ClosedXML.Excel; + +using dosymep.SimpleServices; + +namespace RevitClassifierParameters.Models; + +/// +/// Базовый класс для чтения данных из Excel-файла (.xlsx) через ClosedXML. +/// Инкапсулирует открытие книги и чтение диапазона первого листа +/// в двумерный массив значений ячеек. +/// +/// Тип результата разбора данных листа. +internal abstract class ExcelReaderBase { + protected readonly ILocalizationService _localizationService; + protected readonly IMessageBoxService _messageBoxService; + + protected ExcelReaderBase( + ILocalizationService localizationService, + IMessageBoxService messageBoxService) { + _localizationService = localizationService; + _messageBoxService = messageBoxService; + } + + /// + /// Номер первой строки с данными (строки выше считаются заголовком). + /// + protected abstract int FirstDataRow { get; } + + /// + /// Количество считываемых столбцов, начиная с первого. + /// + protected abstract int ColumnCount { get; } + + /// + /// Ключ локализации сообщения о пустом листе/файле. + /// + protected abstract string EmptySheetMessageKey { get; } + + public TResult Read(string path) { + using var stream = File.OpenRead(path); + using var workbook = new XLWorkbook(stream); + + var worksheet = workbook.Worksheet(1); + + var lastUsedRow = worksheet.LastRowUsed(XLCellsUsedOptions.AllContents); + int lastRow = lastUsedRow?.RowNumber() ?? 0; + + if(lastRow < FirstDataRow) { + _messageBoxService.Show(_localizationService.GetLocalizedString(EmptySheetMessageKey)); + return default; + } + + var values = ReadValues(worksheet, lastRow); + return BuildResult(values); + } + + /// + /// Строит результат разбора из прочитанного двумерного массива значений ячеек. + /// Индексация массива начинается с 0 по обоим измерениям: + /// первое измерение — строки данных (0..rowCount-1), + /// второе — столбцы (0..ColumnCount-1). + /// + protected abstract TResult BuildResult(object[,] rows); + + protected static string GetString(object value) { + return value?.ToString()?.Trim(); + } + + /// + /// Читает диапазон данных листа в двумерный массив с 0-based индексацией. + /// Размерности массива равны фактическому числу строк данных и . + /// + private object[,] ReadValues(IXLWorksheet worksheet, int lastRow) { + int rowCount = lastRow - FirstDataRow + 1; + var values = new object[rowCount, ColumnCount]; + + for(int r = 0; r < rowCount; r++) { + int sheetRow = FirstDataRow + r; + for(int c = 0; c < ColumnCount; c++) { + var cell = worksheet.Cell(sheetRow, c + 1); + values[r, c] = GetCellValue(cell); + } + } + return values; + } + + private static string GetCellValue(IXLCell cell) { + return cell.IsEmpty() ? null : cell.GetString(); + } +} diff --git a/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeExcelReader.cs b/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeExcelReader.cs new file mode 100644 index 000000000..25555ad57 --- /dev/null +++ b/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeExcelReader.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +using dosymep.SimpleServices; + +namespace RevitClassifierParameters.Models.FacadeType; + +/// +/// Читает Excel-файл с правилами заполнения типа фасада. +/// Ожидается три столбца: +/// 1 - характеристика функции (со скобками, например "(ФМ)"); +/// 2 - сокращение основного материала (например "ОК"); +/// 3 - значение для записи в параметр. +/// Первая строка — заголовок, данные начинаются со второй строки. +/// +internal class FacadeTypeExcelReader : ExcelReaderBase> { + public FacadeTypeExcelReader( + ILocalizationService localizationService, + IMessageBoxService messageBoxService) + : base(localizationService, messageBoxService) { + } + + protected override int FirstDataRow => 2; + + protected override int ColumnCount => 3; + + protected override string EmptySheetMessageKey => "Reader.FacadeTypeFileEmpty"; + + protected override List BuildResult(object[,] rows) { + var result = new List(); + + int rowCount = rows.GetLength(0); + + for(int row = 0; row < rowCount; row++) { + string function = GetString(rows[row, 0]); + string material = GetString(rows[row, 1]); + string value = GetString(rows[row, 2]); + + if(string.IsNullOrWhiteSpace(function) + || string.IsNullOrWhiteSpace(material) + || string.IsNullOrWhiteSpace(value)) { + continue; + } + + result.Add(new FacadeTypeItem { + FunctionCharacteristic = Normalize(function), + MaterialAbbreviation = Normalize(material), + Value = value + }); + } + return result; + } + + /// + /// Приводит ключевое поле правила (характеристику функции или сокращение материала) + /// к единому виду для последующего сравнения с распарсенным именем типоразмера стены. + /// + private static string Normalize(string value) { + return (value ?? string.Empty).Trim().ToUpperInvariant(); + } +} diff --git a/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeItem.cs b/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeItem.cs new file mode 100644 index 000000000..e9375bec1 --- /dev/null +++ b/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeItem.cs @@ -0,0 +1,23 @@ +namespace RevitClassifierParameters.Models.FacadeType; + +/// +/// Строка правил заполнения типа фасада, прочитанная из Excel-файла. +/// +internal class FacadeTypeItem { + /// + /// Характеристика функции (столбец 1) хранится со скобками, например "(ФМ)". + /// Приводится к нормализованному виду (Trim + верхний регистр) при чтении из Excel. + /// + public string FunctionCharacteristic { get; set; } + + /// + /// Сокращение основного материала (столбец 2), например "ОК". + /// Приводится к нормализованному виду (Trim + верхний регистр) при чтении из Excel. + /// + public string MaterialAbbreviation { get; set; } + + /// + /// Значение (столбец 3), которое записывается в параметр стены. + /// + public string Value { get; set; } +} diff --git a/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeNameParser.cs b/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeNameParser.cs new file mode 100644 index 000000000..6fe025d12 --- /dev/null +++ b/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeNameParser.cs @@ -0,0 +1,70 @@ +using System.Text.RegularExpressions; + +namespace RevitClassifierParameters.Models.FacadeType; + +/// +/// Результат разбора имени типоразмера стены. +/// +internal readonly struct FacadeTypeNameParts { + public FacadeTypeNameParts(string function, string material) { + Function = function; + Material = material; + } + + /// + /// Характеристика функции (со скобками), например "(ФМ)". + /// + public string Function { get; } + + /// + /// Сокращение основного материала, например "ОК". + /// + public string Material { get; } + + /// + /// Успешно ли извлечены обе подстроки. + /// + public bool IsValid => + !string.IsNullOrWhiteSpace(Function) && !string.IsNullOrWhiteSpace(Material); +} + +/// +/// Разбирает имя типоразмера стены вида +/// "(ФМ) ОК-5 15 (АРМСТК ФДШ-10 ОК-5) RAL #### Цоколь" на две ключевые подстроки: +/// характеристику функции в первых скобках ("(ФМ)") +/// и сокращение основного материала после скобок до первого дефиса ("ОК"). +/// +internal class FacadeTypeNameParser { + // Захватывает первые круглые скобки с содержимым и весь остаток строки после них. + private static readonly Regex _regex = + new(@"^\s*(?\([^)]*\))\s*(?.*)$", RegexOptions.Compiled); + + /// + /// Разбирает имя типоразмера стены. + /// + public FacadeTypeNameParts Parse(string typeName) { + if(string.IsNullOrWhiteSpace(typeName)) { + return default; + } + + var match = _regex.Match(typeName); + if(!match.Success) { + return default; + } + + string function = match.Groups["function"].Value.Trim(); + string material = ExtractMaterial(match.Groups["rest"].Value); + + return new FacadeTypeNameParts(function, material); + } + + private static string ExtractMaterial(string rest) { + if(string.IsNullOrWhiteSpace(rest)) { + return string.Empty; + } + + int dashIndex = rest.IndexOf('-'); + string material = dashIndex >= 0 ? rest.Substring(0, dashIndex) : rest; + return material.Trim(); + } +} diff --git a/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeSetter.cs b/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeSetter.cs new file mode 100644 index 000000000..d97c5e39b --- /dev/null +++ b/src/RevitClassifierParameters/Models/FacadeType/FacadeTypeSetter.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; + +using dosymep.Revit; +using dosymep.SimpleServices; + +namespace RevitClassifierParameters.Models.FacadeType; + +/// +/// Заполняет параметр типа фасада у стен проекта по правилам, прочитанным из Excel. +/// Логика сопоставления имени типоразмера стены с правилами и записи значений +/// сосредоточена в этом классе. +/// +internal class FacadeTypeSetter { + private readonly RevitRepository _revitRepository; + private readonly ILocalizationService _localizationService; + private readonly FacadeTypeNameParser _facadeTypeNameParser = new(); + + public FacadeTypeSetter(RevitRepository revitRepository, ILocalizationService localizationService) { + _revitRepository = revitRepository; + _localizationService = localizationService; + } + + /// + /// Перебирает стены проекта и, если имя типоразмера содержит на нужных местах + /// характеристику функции и сокращение основного материала из файла правил, + /// записывает соответствующее значение в параметр с именем . + /// + /// Имя параметра стены, в который записывается тип фасада. + /// Правила заполнения типа фасада, прочитанные из Excel. + public void SetFacadeType(string paramName, IReadOnlyList facadeTypes) { + if(string.IsNullOrEmpty(paramName) || facadeTypes is null || facadeTypes.Count == 0) { + return; + } + + using var transaction = _revitRepository.Document.StartTransaction( + _localizationService.GetLocalizedString("Transaction.SetFacadeType")); + + foreach(var wall in _revitRepository.GetWalls()) { + string typeName = _revitRepository.Document.GetElement(wall.GetTypeId())?.Name ?? string.Empty; + var parts = _facadeTypeNameParser.Parse(typeName); + if(!parts.IsValid) { + continue; + } + + // Ключевые поля правил уже нормализованы при чтении из Excel, + // поэтому здесь нормализуем только подстроки, извлечённые из имени типоразмера стены. + string function = parts.Function.Trim().ToUpperInvariant(); + string material = parts.Material.Trim().ToUpperInvariant(); + + var rule = facadeTypes.FirstOrDefault(x => + x.FunctionCharacteristic == function + && x.MaterialAbbreviation == material); + if(rule is null) { + continue; + } + + var param = wall.LookupParameter(paramName); + if(param == null || param.IsReadOnly || param.StorageType != StorageType.String) { + continue; + } + + if(param.AsString() != rule.Value) { + param.Set(rule.Value); + } + } + + transaction.Commit(); + } +} diff --git a/src/RevitClassifierParameters/Models/MaterialClassifier/ClassifierExcelReader.cs b/src/RevitClassifierParameters/Models/MaterialClassifier/ClassifierExcelReader.cs new file mode 100644 index 000000000..81de29b14 --- /dev/null +++ b/src/RevitClassifierParameters/Models/MaterialClassifier/ClassifierExcelReader.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Linq; + +using dosymep.SimpleServices; + +using RevitClassifierParameters.Models.Work; + +namespace RevitClassifierParameters.Models.MaterialClassifier; + +internal class ClassifierExcelReader : ExcelReaderBase> { + public ClassifierExcelReader( + ILocalizationService localizationService, + IMessageBoxService messageBoxService) + : base(localizationService, messageBoxService) { + } + + protected override int FirstDataRow => 3; + + protected override int ColumnCount => 3; + + protected override string EmptySheetMessageKey => "Reader.ClassifierSheetEmpty"; + + protected override List BuildResult(object[,] rows) { + var result = new List(); + var groups = new List(); + + int rowCount = rows.GetLength(0); + + for(int row = 0; row < rowCount; row++) { + string code = GetString(rows[row, 0]); + if(string.IsNullOrWhiteSpace(code)) { + continue; + } + + string name = GetString(rows[row, 1]); + string unit = GetString(rows[row, 2]); + + if(string.IsNullOrWhiteSpace(unit)) { + AddGroup(result, groups, code, name); + } else { + AddWork(groups, code, name, unit); + } + } + return result; + } + + private void AddGroup( + List roots, + List groups, + string code, + string name) { + var group = new WorkGroup { + Code = code, + Name = name + }; + + groups.Add(group); + + var parent = FindParentGroup(groups, code); + if(parent != null) { + group.ParentWorkGroup = parent; + parent.ChildWorkGroups.Add(group); + } else { + roots.Add(group); + } + } + + private void AddWork( + List groups, + string code, + string name, + string unit) { + var parent = FindParentGroup(groups, code); + + parent?.ChildWorks.Add(new WorkItem { + Code = code, + Name = name, + Unit = unit, + ParentWorkGroup = parent + }); + } + + private static WorkGroup FindParentGroup( + List groups, + string code) { + string current = code; + int separatorIndex; + + while((separatorIndex = current.LastIndexOf('.')) >= 0) { + current = current.Substring(0, separatorIndex); + var parent = groups.FirstOrDefault(g => g.Code == current); + if(parent != null) { + return parent; + } + } + return null; + } +} diff --git a/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialParamSetter.cs b/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialParamSetter.cs new file mode 100644 index 000000000..b2579bdc5 --- /dev/null +++ b/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialParamSetter.cs @@ -0,0 +1,243 @@ +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; + +using dosymep.Bim4Everyone; +using dosymep.Bim4Everyone.SharedParams; +using dosymep.Revit; +using dosymep.SimpleServices; + +using RevitClassifierParameters.Models.Work; + +namespace RevitClassifierParameters.Models.MaterialClassifier; + +internal class MaterialParamSetter { + private static readonly RevitParam _chapterParameter = SharedParamsConfig.Instance.MtrChapterTitle; + private static readonly RevitParam _workTitleParameter = SharedParamsConfig.Instance.MtrWorkTitle; + private static readonly RevitParam _unitParameter = SharedParamsConfig.Instance.MtrMeasurementUnit; + private static readonly RevitParam _calculationTypeParameter = SharedParamsConfig.Instance.MtrCountingType; + + private static readonly Dictionary _calculationTypeDict = new() { + ["м"] = 1, + ["м²"] = 2, + ["м³"] = 3, + ["шт."] = 4 + }; + + private readonly RevitRepository _revitRepository; + private readonly MaterialReportService _materialReportService; + private readonly ILocalizationService _localizationService; + + public MaterialParamSetter( + RevitRepository revitRepository, + MaterialReportService materialReportService, + ILocalizationService localizationService) { + _revitRepository = revitRepository; + _materialReportService = materialReportService; + _localizationService = localizationService; + } + + /// + /// Задает значения параметрам материалов + /// + /// Выбранные пользователем коды для обработки + /// Работы из классификатора + /// Материалы в проекте + /// Работа с архитектурными группами классификатора + public void SetParamValue( + HashSet activeCodes, + List classifierWorks, + List materialInPj, + bool forAr) { + + // Сопоставляем материалы с работами Классификатора + var revitMaterials = MatchMaterials(activeCodes, materialInPj, classifierWorks); + + SetClassifierParameters(revitMaterials, forAr); + } + + /// + /// Сопоставляет материалы проекта с работами Классификатора по ключевой заметке. + /// Обрабатываются только материалы, чей код начинается с одного из выбранных кодов групп. + /// + private List MatchMaterials( + HashSet activeCodes, + List materialInPj, + List classifierWorks) { + + var revitMaterials = new List(); + + foreach(var material in materialInPj) { + string keynote = material.GetParamValueOrDefault(BuiltInParameter.KEYNOTE_PARAM); + + // Отсеиваем ситуации, когда у материала не указана Ключевая заметка (код работы) + if(string.IsNullOrEmpty(keynote)) { + _materialReportService.Add(MaterialReportStatus.NoWorkCode, string.Empty, material.Name); + continue; + } + + // Обрабатываем только материалы, чей код принадлежит одному из выбранных кодов групп + if(!activeCodes.Any(code => CodeStartsWith(keynote, code))) { + continue; + } + + // Отсеиваем ситуации, когда Классификатор не содержит указанный в материале код + var work = FindWork(classifierWorks, keynote); + if(work is null) { + _materialReportService.Add(MaterialReportStatus.ClassifierCodeNotFound, keynote, material.Name); + continue; + } + + revitMaterials.Add(new RevitMaterial(keynote, material, work)); + } + + return revitMaterials; + } + + /// + /// Ищет работу по коду, спускаясь по дереву классификатора. + /// Использует факт, что код работы включает код своей группы как префикс, + /// поэтому заходит только в ту ветку, чей код группы является префиксом искомого кода. + /// + private WorkItem FindWork(List workGroups, string code) { + if(workGroups is null) { + return null; + } + + foreach(var workGroup in workGroups) { + // Код работы всегда включает код группы как префикс, + // поэтому неподходящие ветки пропускаем целиком + if(!CodeStartsWith(code, workGroup.Code)) { + continue; + } + + var work = workGroup.ChildWorks.FirstOrDefault(w => w.Code == code); + if(work != null) { + return work; + } + + var found = FindWork(workGroup.ChildWorkGroups, code); + if(found != null) { + return found; + } + } + + return null; + } + + /// + /// Проверяет, что код совпадает с префиксом или является его дочерним кодом + /// (совпадение по границе сегмента, разделённого точкой). + /// Пример: для префикса "г02.03" -> true для "г02.03" и "г02.03.01", false для "г02.030". + /// + private bool CodeStartsWith(string code, string prefix) { + return code == prefix || code.StartsWith(prefix + "."); + } + + /// + /// Заполняет параметры Классификатора у материалов в одной транзакции. + /// + private void SetClassifierParameters(List revitMaterials, bool forAr) { + using var transaction = _revitRepository.Document.StartTransaction( + _localizationService.GetLocalizedString("Transaction.SetClassifierParameters")); + foreach(var revitMaterial in revitMaterials) { + var material = revitMaterial.Material; + try { + bool edited = false; + var work = revitMaterial.Work; + string chapterName = GetChapterName(work, forAr); + + edited |= SetStringParam(material, _chapterParameter, chapterName); + edited |= SetStringParam(material, _workTitleParameter, work.Name); + edited |= SetStringParam(material, _unitParameter, work.Unit); + edited |= SetCalculationTypeParam(material, work.Unit); + edited |= SetStringParam( + material, + BuiltInParameter.ALL_MODEL_DESCRIPTION, + revitMaterial.MaterialDescription); + + _materialReportService.Add( + edited ? MaterialReportStatus.Edited : MaterialReportStatus.NotEdited, + revitMaterial.Keynote, + material.Name); + } catch { + _materialReportService.Add(MaterialReportStatus.Error, revitMaterial.Keynote, material.Name); + } + } + + transaction.Commit(); + } + + /// + /// Возвращает наименование главы для материала. + /// forAr = true -> наименование ближайшей главы (непосредственный родитель работы). + /// forAr = false -> наименование главы самого верхнего уровня + /// (поднимаемся по ParentWorkGroup, пока он не станет null). + /// + private string GetChapterName(WorkItem work, bool forAr) { + var chapter = work.ParentWorkGroup; + if(chapter is null) { + return string.Empty; + } + + if(forAr) { + return chapter.Name; + } + + while(chapter.ParentWorkGroup != null) { + chapter = chapter.ParentWorkGroup; + } + return chapter.Name; + } + + /// + /// Устанавливает значение параметра "Тип подсчета" по единице измерения. + /// При валидной единице пишется число, иначе - локализованная строка ошибки. + /// Возвращает true, если параметр был изменён. + /// + private bool SetCalculationTypeParam(Material material, string unit) { + var param = material.GetParam(_calculationTypeParameter); + + if(unit != null && _calculationTypeDict.TryGetValue(unit, out int value)) { + if(param.AsValueString() == value.ToString()) { + return false; + } + material.SetParamValue(_calculationTypeParameter, value); + return true; + } + + string calculationTypeError = _localizationService.GetLocalizedString("CalculationType.Error"); + if(param.AsValueString() == calculationTypeError) { + return false; + } + material.SetParamValue(_calculationTypeParameter, calculationTypeError); + return true; + } + + /// + /// Устанавливает строковое значение параметра, только если оно отличается от текущего. + /// Возвращает true, если параметр был изменён. + /// + private bool SetStringParam(Material material, RevitParam revitParam, string value) { + var param = material.GetParam(revitParam); + if(param.AsValueString() == value) { + return false; + } + material.SetParamValue(revitParam, value ?? string.Empty); + return true; + } + + /// + /// Устанавливает строковое значение системного параметра, только если оно отличается от текущего. + /// Возвращает true, если параметр был изменён. + /// + private bool SetStringParam(Material material, BuiltInParameter builtInParameter, string value) { + var param = material.GetParam(builtInParameter); + if(param.AsValueString() == value) { + return false; + } + material.SetParamValue(builtInParameter, value ?? string.Empty); + return true; + } +} diff --git a/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialReportItem.cs b/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialReportItem.cs new file mode 100644 index 000000000..136123347 --- /dev/null +++ b/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialReportItem.cs @@ -0,0 +1,64 @@ +namespace RevitClassifierParameters.Models.MaterialClassifier; + +/// +/// Статус обработки материала. +/// +internal enum MaterialReportStatus { + /// + /// Параметры материала были изменены. + /// + Edited, + + /// + /// Параметры материала не изменялись (значения уже актуальны). + /// + NotEdited, + + /// + /// У материала не указан код работы (ключевая заметка). + /// + NoWorkCode, + + /// + /// Код работы материала не найден в Классификаторе. + /// + ClassifierCodeNotFound, + + /// + /// При записи параметров произошла ошибка. + /// + Error +} + +/// +/// Запись отчёта по обработке одного материала. +/// Аналог строки отчёта из Python-плагина: [Статус, Код работы, Имя материала]. +/// +internal class MaterialReportItem { + /// + /// Создает запись отчёта. + /// + /// Статус обработки материала. + /// Код работы (ключевая заметка материала). + /// Имя материала. + public MaterialReportItem(MaterialReportStatus status, string workCode, string materialName) { + Status = status; + WorkCode = workCode; + MaterialName = materialName; + } + + /// + /// Статус обработки материала. + /// + public MaterialReportStatus Status { get; } + + /// + /// Код работы (ключевая заметка материала). + /// + public string WorkCode { get; } + + /// + /// Имя материала. + /// + public string MaterialName { get; } +} diff --git a/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialReportService.cs b/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialReportService.cs new file mode 100644 index 000000000..a23a9d864 --- /dev/null +++ b/src/RevitClassifierParameters/Models/MaterialClassifier/MaterialReportService.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace RevitClassifierParameters.Models.MaterialClassifier; + +/// +/// Сервис сбора отчёта по обработке материалов. +/// +internal class MaterialReportService { + private readonly List _items = []; + + /// + /// Собранные записи отчёта. + /// + public IReadOnlyList Items => _items; + + /// + /// Добавляет запись в отчёт. + /// + /// Статус обработки материала. + /// Код работы (ключевая заметка материала). + /// Имя материала. + public void Add(MaterialReportStatus status, string workCode, string materialName) { + _items.Add(new MaterialReportItem(status, workCode, materialName)); + } + + /// + /// Очищает отчёт. + /// + public void Clear() { + _items.Clear(); + } +} diff --git a/src/RevitClassifierParameters/Models/MaterialClassifier/RevitMaterial.cs b/src/RevitClassifierParameters/Models/MaterialClassifier/RevitMaterial.cs new file mode 100644 index 000000000..d09178a70 --- /dev/null +++ b/src/RevitClassifierParameters/Models/MaterialClassifier/RevitMaterial.cs @@ -0,0 +1,66 @@ +using Autodesk.Revit.DB; + +using RevitClassifierParameters.Models.Work; + +namespace RevitClassifierParameters.Models.MaterialClassifier; + +/// +/// Класс-оболочка для хранения информации о материале Revit и связанной с ним работе из Классификатора. +/// +internal class RevitMaterial { + private const char _mark = '_'; + + /// + /// Создает обёртку материала. + /// + /// Ключевая заметка материала (код работы). + /// Материал Revit. + /// Работа из Классификатора, параметры которой нужно назначить. + public RevitMaterial(string keynote, Material material, WorkItem work) { + Keynote = keynote; + Material = material; + Work = work; + MaterialDescription = GetMaterialDescription(); + } + + /// + /// Ключевая заметка материала (код работы). + /// + public string Keynote { get; } + + /// + /// Материал Revit. + /// + public Material Material { get; } + + /// + /// Работа из Классификатора, параметры которой нужно назначить. + /// + public WorkItem Work { get; } + + /// + /// Описание материала для параметра "Материал: Описание". + /// + public string MaterialDescription { get; } + + /// + /// Возвращает строку для заполнения параметра "Материал: Описание". + /// Строка содержится в имени материала между первым и вторым нижним подчеркиванием. + /// Пример: "г02.04.01.04_Бетон_Выше 0_Устройство монолитных ж/б пилонов" -> "Бетон". + /// + private string GetMaterialDescription() { + string materialName = Material.Name ?? string.Empty; + + int firstIndex = materialName.IndexOf(_mark); + if(firstIndex < 0) { + return string.Empty; + } + + int secondIndex = materialName.IndexOf(_mark, firstIndex + 1); + if(secondIndex < 0) { + return string.Empty; + } + + return materialName.Substring(firstIndex + 1, secondIndex - firstIndex - 1).Trim(); + } +} diff --git a/src/RevitClassifierParameters/Models/PluginConfig.cs b/src/RevitClassifierParameters/Models/PluginConfig.cs new file mode 100644 index 000000000..88ff2a644 --- /dev/null +++ b/src/RevitClassifierParameters/Models/PluginConfig.cs @@ -0,0 +1,102 @@ +using System.IO; + +using dosymep.Bim4Everyone; +using dosymep.Bim4Everyone.ProjectConfigs; + +using pyRevitLabs.Json; + +namespace RevitClassifierParameters.Models; + +/// +/// Класс конфигурации плагина. +/// (Если не используется удалить) +/// +internal class PluginConfig : ProjectConfig { + /// + /// Системное свойство конфигурации. (Не трогать) + /// + [JsonIgnore] + public override string ProjectConfigPath { get; set; } + + /// + /// Системное свойство конфигурации. (Не трогать) + /// + [JsonIgnore] + public override IConfigSerializer Serializer { get; set; } + + /// + /// Метод создания конфигурации плагина. + /// + /// + /// Сериализатор конфигурации. + /// Возвращает прочитанную конфигурацию плагина, либо созданный конфиг по умолчанию. + /// + public static PluginConfig GetPluginConfig(IConfigSerializer configSerializer) { + return new ProjectConfigBuilder() + .SetSerializer(configSerializer) + .SetPluginName(nameof(RevitClassifierParameters)) + .SetRevitVersion(ModuleEnvironment.RevitVersion) + .SetProjectConfigName(nameof(PluginConfig) + ".json") + .Build(); + } +} + +/// +/// Настройки проекта. +/// В настройках проекта обычно хранится выбор пользователя в основном окне плагина. +/// +/// +/// Проектом по умолчанию является текст до первого нижнего подчеркивания. +/// +/// https://github.com/dosymep/dosymep.Revit/blob/master/src/dosymep.Bim4Everyone/ProjectConfigs/ProjectConfig.cs#L102 +/// Если плагин работает без открытых проектов, +/// то требуется данный класс удалять из проекта, +/// как сделано в плагине RevitServerFolders +/// https://github.com/Bim4Everyone/RevitPlugins/blob/master/src/RevitServerFolders/Models/PluginConfig.cs#L8 +/// +internal class RevitSettings : ProjectSettings { + /// + /// Наименование проекта. Системное свойство. (Не трогать) + /// + public override string ProjectName { get; set; } + + public string ExcelClassifierPath { get; set; } + public string ExcelFacadeTypePath { get; set; } + public bool WorkWithFoundationCode { get; set; } + public bool WorkWithConstrBelowZeroCode { get; set; } + public bool WorkWithConstrAboveZeroCode { get; set; } + public bool WorkWithMasonryCode { get; set; } + public bool WorkWithRoofCode { get; set; } + public bool WorkWithFacadeCode { get; set; } + public bool WorkWithFacadeType { get; set; } + public string ParamNameForFacadeType { get; set; } +} + +/// +/// Системная конфигурация плагина. +/// Содержит стандартные данные, которые некорректно хранить в конфигурации проекта. +/// +internal class SystemPluginConfig { + // Основная папка с ресурсами надстройки + private readonly string _mainFolder = + @"W:\Проектный институт\Отд.стандарт.BIM и RD\BIM-Ресурсы\5-Надстройки\Bim4Everyone\A101"; + + // Папка с файлом Классификатора + private readonly string _classifierDirectory = + @"W:\Проектный институт\Проектные Группы\Типовые ТЗ\BIM-стандарт A101"; + + /// + /// Стандартный путь к файлу правил заполнения типа фасада. + /// + public string FacadeTypeFilePath => Path.Combine( + _mainFolder, + ModuleEnvironment.RevitVersion, + "RevitClassifierParameters", + "Правила заполнения типа фасада.xlsx"); + + /// + /// Стандартная папка с файлом Классификатора, + /// открываемая по умолчанию в диалоге выбора файла. + /// + public string ClassifierDirectoryPath => _classifierDirectory; +} diff --git a/src/RevitClassifierParameters/Models/RevitRepository.cs b/src/RevitClassifierParameters/Models/RevitRepository.cs new file mode 100644 index 000000000..8302806a5 --- /dev/null +++ b/src/RevitClassifierParameters/Models/RevitRepository.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.ApplicationServices; +using Autodesk.Revit.DB; +using Autodesk.Revit.UI; + +using dosymep.SimpleServices; + +namespace RevitClassifierParameters.Models; + +/// +/// Класс доступа к документу и приложению Revit. +/// +/// +/// В случае если данный класс разрастается, рекомендуется его разделить на несколько. +/// +internal class RevitRepository { + private readonly ILocalizationService _localizationService; + private readonly IMessageBoxService _messageBoxService; + + /// + /// Создает экземпляр репозитория. + /// + /// Класс доступа к интерфейсу Revit. + public RevitRepository( + UIApplication uiApplication, + ILocalizationService localizationService, + IMessageBoxService messageBoxService) { + UIApplication = uiApplication; + _localizationService = localizationService; + _messageBoxService = messageBoxService; + } + + /// + /// Класс доступа к интерфейсу Revit. + /// + public UIApplication UIApplication { get; } + + /// + /// Класс доступа к интерфейсу документа Revit. + /// + public UIDocument ActiveUIDocument => UIApplication.ActiveUIDocument; + + /// + /// Класс доступа к приложению Revit. + /// + public Application Application => UIApplication.Application; + + /// + /// Класс доступа к документу Revit. + /// + public Document Document => ActiveUIDocument.Document; + + + public List GetElementMaterials() { + var materials = GetElementInPj() + .SelectMany(elem => elem.GetMaterialIds(false)) + .Distinct() + .Select(id => Document.GetElement(id) as Material) + .Where(material => material != null) + .ToList(); + + if(materials.Count == 0) { + _messageBoxService.Show(_localizationService.GetLocalizedString("Repository.NoMaterialsForElements")); + } + return materials; + } + + private List GetElementInPj() { + var elements = new FilteredElementCollector(Document) + .WhereElementIsNotElementType() + .ToElements() + .ToList(); + if(elements.Count != 0) { + return elements; + } + _messageBoxService.Show(_localizationService.GetLocalizedString("Repository.NoElementsInProject")); + return []; + } + + public List GetParametersForFacadeType() { + var wall = new FilteredElementCollector(Document) + .OfClass(typeof(Wall)) + .WhereElementIsNotElementType() + .FirstOrDefault(); + + if(wall == null) { + return []; + } + + return wall.Parameters + .Cast() + .Where(p => p.StorageType == StorageType.String) + .Where(p => !p.IsReadOnly) + .OrderBy(p => p.Definition.Name) + .ToList(); + } + + /// + /// Возвращает все экземпляры стен в проекте. + /// + public List GetWalls() { + return new FilteredElementCollector(Document) + .OfClass(typeof(Wall)) + .WhereElementIsNotElementType() + .Cast() + .ToList(); + } +} diff --git a/src/RevitClassifierParameters/Models/Work/WorkGroup.cs b/src/RevitClassifierParameters/Models/Work/WorkGroup.cs new file mode 100644 index 000000000..eb012a391 --- /dev/null +++ b/src/RevitClassifierParameters/Models/Work/WorkGroup.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace RevitClassifierParameters.Models.Work; + +internal class WorkGroup : WorkItem { + public List ChildWorkGroups { get; set; } = []; + public List ChildWorks { get; set; } = []; +} diff --git a/src/RevitClassifierParameters/Models/Work/WorkGroupCode.cs b/src/RevitClassifierParameters/Models/Work/WorkGroupCode.cs new file mode 100644 index 000000000..9b95113bc --- /dev/null +++ b/src/RevitClassifierParameters/Models/Work/WorkGroupCode.cs @@ -0,0 +1,10 @@ +namespace RevitClassifierParameters.Models.Work; + +internal class WorkGroupCode { + public string FoundationCode { get; set; } = "г02.02"; + public string ConstrBelowZeroCode { get; set; } = "г02.03"; + public string ConstrAboveZeroCode { get; set; } = "г02.04"; + public string MasonryCode { get; set; } = "г02.05"; + public string RoofCode { get; set; } = "г02.06"; + public string FacadeCode { get; set; } = "г02.07"; +} diff --git a/src/RevitClassifierParameters/Models/Work/WorkItem.cs b/src/RevitClassifierParameters/Models/Work/WorkItem.cs new file mode 100644 index 000000000..af9661dff --- /dev/null +++ b/src/RevitClassifierParameters/Models/Work/WorkItem.cs @@ -0,0 +1,8 @@ +namespace RevitClassifierParameters.Models.Work; + +internal class WorkItem { + public string Code { get; set; } + public string Name { get; set; } + public string Unit { get; set; } + public WorkGroup ParentWorkGroup { get; set; } +} diff --git a/src/RevitClassifierParameters/README.md b/src/RevitClassifierParameters/README.md new file mode 100644 index 000000000..38f17c09e --- /dev/null +++ b/src/RevitClassifierParameters/README.md @@ -0,0 +1,12 @@ +# RevitClassifierParameters (Параметры ВОР) +Плагин предназначен для автоматизации процесса заполнения параметров ВОР в Revit. + +# Сборка проекта +``` +nuke compile --profile RevitClassifierParameters +``` + +# Публикация проекта +``` +nuke publish --profile RevitClassifierParameters +``` diff --git a/src/RevitClassifierParameters/RevitArParameterClassifierCommand.cs b/src/RevitClassifierParameters/RevitArParameterClassifierCommand.cs new file mode 100644 index 000000000..88c5ae330 --- /dev/null +++ b/src/RevitClassifierParameters/RevitArParameterClassifierCommand.cs @@ -0,0 +1,127 @@ +using System.Globalization; +using System.Reflection; +using System.Windows; + +using Autodesk.Revit.Attributes; +using Autodesk.Revit.UI; + +using dosymep.Bim4Everyone; +using dosymep.Bim4Everyone.ProjectConfigs; +using dosymep.Bim4Everyone.SimpleServices; +using dosymep.WpfCore.Ninject; +using dosymep.WpfUI.Core.Ninject; + +using Ninject; + +using RevitClassifierParameters.Models; +using RevitClassifierParameters.Models.FacadeType; +using RevitClassifierParameters.Models.MaterialClassifier; +using RevitClassifierParameters.Models.Work; +using RevitClassifierParameters.ViewModels; +using RevitClassifierParameters.Views; + +namespace RevitClassifierParameters; + +/// +/// Класс команды Revit плагина. +/// +/// +/// В данном классе должна быть инициализация контейнера плагина и указание названия команды. +/// +[Transaction(TransactionMode.Manual)] +public class RevitArParameterClassifierCommand : BasePluginCommand { + /// + /// Инициализирует команду плагина. + /// + public RevitArParameterClassifierCommand() { + PluginName = "Параметры ВОР АР"; + } + + /// + /// Метод выполнения основного кода плагина. + /// + /// Интерфейс взаимодействия с Revit. + /// + /// В случаях, когда не используется конфигурация + /// или локализация требуется удалять их использование полностью во всем проекте. + /// + protected override void Execute(UIApplication uiApplication) { + // Создание контейнера зависимостей плагина с сервисами из платформы + using IKernel kernel = uiApplication.CreatePlatformServices(); + + // Настройка доступа к Revit + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + // Настройка конфигурации плагина + kernel.Bind() + .ToMethod(c => PluginConfig.GetPluginConfig(c.Kernel.Get())); + + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + // Системная конфигурация плагина + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + // Настройка окна отчёта + kernel.Bind() + .ToSelf() + .InSingletonScope(); + + kernel.Bind() + .ToSelf() + .InSingletonScope() + .WithPropertyValue(nameof(Window.DataContext), + c => c.Kernel.Get()); + + // Используем сервис обновления тем для WPF UI + kernel.UseWpfUIThemeUpdater(); + + // Настройка запуска окна + kernel.BindMainWindow(); + + // Настройка локализации, + // получение имени сборки откуда брать текст + string assemblyName = Assembly.GetExecutingAssembly().GetName().Name; + + // Настройка локализации, + // установка дефолтной локализации "ru-RU" + kernel.UseWpfLocalization( + $"/{assemblyName};component/assets/localization/language.xaml", + CultureInfo.GetCultureInfo("ru-RU")); + + // Настройка сервиса окошек сообщений + kernel.UseWpfUIMessageBox(); + + // Сервис открытия диалогового окна для чтения файла Классификатора + kernel.UseWpfOpenFileDialog( + filter: "Excel files (*.xlsx)|*.xlsx|All files (*.*)|*.*"); + + // Вызывает стандартное уведомление + Notification(kernel.Get()); + } +} diff --git a/src/RevitClassifierParameters/RevitClassifierParameters.csproj b/src/RevitClassifierParameters/RevitClassifierParameters.csproj new file mode 100644 index 000000000..d48c6083e --- /dev/null +++ b/src/RevitClassifierParameters/RevitClassifierParameters.csproj @@ -0,0 +1,7 @@ + + + true + true + 12 + + diff --git a/src/RevitClassifierParameters/ViewModels/ArParameterClassifierVM.cs b/src/RevitClassifierParameters/ViewModels/ArParameterClassifierVM.cs new file mode 100644 index 000000000..cfd6d9c9e --- /dev/null +++ b/src/RevitClassifierParameters/ViewModels/ArParameterClassifierVM.cs @@ -0,0 +1,240 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Windows.Input; + +using Autodesk.Revit.DB; + +using dosymep.Bim4Everyone.SharedParams; +using dosymep.SimpleServices; +using dosymep.WPF.Commands; + +using RevitClassifierParameters.Models; +using RevitClassifierParameters.Models.FacadeType; +using RevitClassifierParameters.Models.MaterialClassifier; +using RevitClassifierParameters.Models.Work; +using RevitClassifierParameters.Views; + +namespace RevitClassifierParameters.ViewModels; + +internal class ArParameterClassifierVM : ParameterClassifierVM { + private readonly FacadeTypeSetter _facadeTypeSetter; + + /// + /// Стандартный путь к файлу правил заполнения типа фасада. + /// + private string _excelFacadeTypePath = string.Empty; + /// + /// Стандартное наименование параметра стен на экземпляре, куда нужно заполнить тип фасада. + /// + private string _facadeTypeParamName = SharedParamsConfig.Instance.Grouping.Name; + private bool _workWithMasonryCode; + private bool _workWithRoofCode; + private bool _workWithFacadeCode; + private bool _workWithFacadeType; + private List _facadeTypes; + private Parameter _paramForFacadeType; + private ObservableCollection _paramsForFacadeType; + + public ArParameterClassifierVM( + PluginConfig pluginConfig, + RevitRepository revitRepository, + ILocalizationService localizationService, + WorkGroupCode workGroupCode, + MaterialParamSetter materialParamSetter, + MaterialReportService materialReportService, + ClassifierExcelReader classifierExcelReader, + FacadeTypeExcelReader facadeTypeExcelReader, + FacadeTypeSetter facadeTypeSetter, + SystemPluginConfig systemPluginConfig, + ReportV reportV, + IOpenFileDialogService openFileDialogService, + IMessageBoxService messageBoxService) : + base(pluginConfig, revitRepository, localizationService, workGroupCode, materialParamSetter, materialReportService, + classifierExcelReader, facadeTypeExcelReader, systemPluginConfig, reportV, openFileDialogService, + messageBoxService) { + + _facadeTypeSetter = facadeTypeSetter; + + _excelFacadeTypePath = _systemPluginConfig.FacadeTypeFilePath; + + ReadFacadeTypeExcelCommand = RelayCommand.Create(RereadFacadeTypeExcel); + } + + public ICommand ReadFacadeTypeExcelCommand { get; } + + public bool WorkWithMasonryCode { + get => _workWithMasonryCode; + set => RaiseAndSetIfChanged(ref _workWithMasonryCode, value); + } + + public bool WorkWithRoofCode { + get => _workWithRoofCode; + set => RaiseAndSetIfChanged(ref _workWithRoofCode, value); + } + + public bool WorkWithFacadeCode { + get => _workWithFacadeCode; + set => RaiseAndSetIfChanged(ref _workWithFacadeCode, value); + } + + public bool WorkWithFacadeType { + get => _workWithFacadeType; + set => RaiseAndSetIfChanged(ref _workWithFacadeType, value); + } + + public List FacadeTypes { + get => _facadeTypes; + private set => RaiseAndSetIfChanged(ref _facadeTypes, value); + } + + public string ExcelFacadeTypePath { + get => _excelFacadeTypePath; + set => RaiseAndSetIfChanged(ref _excelFacadeTypePath, value); + } + + public Parameter ParamForFacadeType { + get => _paramForFacadeType; + set => RaiseAndSetIfChanged(ref _paramForFacadeType, value); + } + + public ObservableCollection ParamsForFacadeType { + get => _paramsForFacadeType; + private set => RaiseAndSetIfChanged(ref _paramsForFacadeType, value); + } + + protected override void LoadView() { + LoadConfig(); + ReadClassifierExcel(); + GetMaterials(); + GetParamsForFacadeType(); + ReadFacadeTypeExcel(); + } + + /// + /// Получение параметров для заполнения типа фасада + /// + private void GetParamsForFacadeType() { + ParamsForFacadeType = new ObservableCollection(_revitRepository.GetParametersForFacadeType()); + ParamForFacadeType = ParamsForFacadeType.FirstOrDefault(p => p.Definition.Name == _facadeTypeParamName); + } + + /// + /// Считывает файл правил заполнения типа фасада. + /// Если путь по умолчанию/из конфига существует — читает без диалога, иначе открывает диалог выбора файла. + /// + private void RereadFacadeTypeExcel() { + OpenFileDialogService.Title = _localizationService.GetLocalizedString("MainWindow.SelectFacadeTypeFile"); + + if(OpenFileDialogService.ShowDialog()) { + // Если пользователь выбрал файл при выборе файла + ExcelFacadeTypePath = OpenFileDialogService.File.FullName; + } else { + // Если пользователь не выбрал файл + MessageBoxService.Show(_localizationService.GetLocalizedString("MainWindow.NoFacadeTypeFileSelected")); + return; + } + + FacadeTypes = _facadeTypeExcelReader.Read(ExcelFacadeTypePath); + if(FacadeTypes is null || FacadeTypes.Count == 0) { + MessageBoxService.Show(_localizationService.GetLocalizedString("MainWindow.NoFacadeTypesFound")); + } + } + + private void ReadFacadeTypeExcel() { + if(string.IsNullOrEmpty(ExcelFacadeTypePath) || !File.Exists(ExcelFacadeTypePath)) { + OpenFileDialogService.Title = _localizationService.GetLocalizedString("MainWindow.SelectFacadeTypeFile"); + + if(OpenFileDialogService.ShowDialog()) { + // Если пользователь выбрал файл при выборе файла + ExcelFacadeTypePath = OpenFileDialogService.File.FullName; + } else { + // Если пользователь не выбрал файл + MessageBoxService.Show(_localizationService.GetLocalizedString("MainWindow.NoFacadeTypeFileSelected")); + return; + } + } + + FacadeTypes = _facadeTypeExcelReader.Read(ExcelFacadeTypePath); + if(FacadeTypes is null || FacadeTypes.Count == 0) { + MessageBoxService.Show(_localizationService.GetLocalizedString("MainWindow.NoFacadeTypesFound")); + } + } + + protected override void AcceptView() { + SaveConfig(); + var activeCodes = GetActiveCodes(); + _materialParamSetter.SetParamValue(activeCodes, CurrentClassifierWorks, MaterialInPj, true); + if(WorkWithFacadeCode + && WorkWithFacadeType + && ParamForFacadeType != null + && FacadeTypes != null + && FacadeTypes.Count != 0) { + _facadeTypeSetter.SetFacadeType(ParamForFacadeType.Definition.Name, FacadeTypes); + } + ShowReport(); + } + + protected override bool CanAcceptView() { + // Если планируем работать с классификатором + if(WorkWithMasonryCode || WorkWithRoofCode || WorkWithFacadeType) { + if(string.IsNullOrEmpty(ExcelClassifierPath)) { + ErrorText = _localizationService.GetLocalizedString("MainWindow.ErrorNoClassifierFile"); + return false; + } + if(CurrentClassifierWorks is null || CurrentClassifierWorks.Count == 0) { + ErrorText = _localizationService.GetLocalizedString("MainWindow.ErrorNoClassifierWorks"); + return false; + } + } + + // Если планируем работать с типами фасадов + if(WorkWithFacadeType) { + if(string.IsNullOrEmpty(ExcelFacadeTypePath)) { + ErrorText = _localizationService.GetLocalizedString("MainWindow.ErrorNoFacadeTypeFile"); + return false; + } + if(FacadeTypes is null || FacadeTypes.Count == 0) { + ErrorText = _localizationService.GetLocalizedString("MainWindow.ErrorNoFacadeTypes"); + return false; + } + if(ParamForFacadeType is null) { + ErrorText = _localizationService.GetLocalizedString("MainWindow.ErrorNoFacadeTypeParam"); + return false; + } + } + + ErrorText = string.Empty; + return true; + } + + private void LoadConfig() { + var setting = _pluginConfig.GetSettings(_revitRepository.Document); + + ExcelClassifierPath = setting?.ExcelClassifierPath ?? string.Empty; + WorkWithMasonryCode = setting?.WorkWithMasonryCode ?? true; + WorkWithRoofCode = setting?.WorkWithRoofCode ?? true; + WorkWithFacadeCode = setting?.WorkWithFacadeCode ?? true; + WorkWithFacadeType = setting?.WorkWithFacadeType ?? true; + + // Для следующих есть стандартное значение, но пользователь может задать иное + _facadeTypeParamName = setting?.ParamNameForFacadeType ?? _facadeTypeParamName; + ExcelFacadeTypePath = setting?.ExcelFacadeTypePath ?? _excelFacadeTypePath; + } + + private void SaveConfig() { + var setting = _pluginConfig.GetSettings(_revitRepository.Document) + ?? _pluginConfig.AddSettings(_revitRepository.Document); + + setting.ExcelClassifierPath = ExcelClassifierPath; + setting.ExcelFacadeTypePath = ExcelFacadeTypePath; + setting.WorkWithMasonryCode = WorkWithMasonryCode; + setting.WorkWithRoofCode = WorkWithRoofCode; + setting.WorkWithFacadeCode = WorkWithFacadeCode; + setting.WorkWithFacadeType = WorkWithFacadeType; + setting.ParamNameForFacadeType = ParamForFacadeType?.Definition.Name ?? _facadeTypeParamName; + + _pluginConfig.SaveProjectConfig(); + } +} diff --git a/src/RevitClassifierParameters/ViewModels/ParameterClassifierVM.cs b/src/RevitClassifierParameters/ViewModels/ParameterClassifierVM.cs new file mode 100644 index 000000000..1d4354908 --- /dev/null +++ b/src/RevitClassifierParameters/ViewModels/ParameterClassifierVM.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows.Input; + +using Autodesk.Revit.DB; + +using dosymep.SimpleServices; +using dosymep.WPF.Commands; +using dosymep.WPF.ViewModels; + +using RevitClassifierParameters.Models; +using RevitClassifierParameters.Models.FacadeType; +using RevitClassifierParameters.Models.MaterialClassifier; +using RevitClassifierParameters.Models.Work; +using RevitClassifierParameters.Views; + +namespace RevitClassifierParameters.ViewModels; + +internal abstract class ParameterClassifierVM : BaseViewModel { + protected readonly PluginConfig _pluginConfig; + protected readonly RevitRepository _revitRepository; + protected readonly ILocalizationService _localizationService; + protected readonly WorkGroupCode _workGroupCode; + protected readonly MaterialParamSetter _materialParamSetter; + protected readonly MaterialReportService _materialReportService; + protected readonly ClassifierExcelReader _classifierExcelReader; + protected readonly FacadeTypeExcelReader _facadeTypeExcelReader; + protected readonly SystemPluginConfig _systemPluginConfig; + protected readonly ReportV _reportV; + + private string _errorText; + private List _currentClassifierWorks; + private List _materialInPj; + protected string _excelClassifierPath; + + protected ParameterClassifierVM( + PluginConfig pluginConfig, + RevitRepository revitRepository, + ILocalizationService localizationService, + WorkGroupCode workGroupCode, + MaterialParamSetter materialParamSetter, + MaterialReportService materialReportService, + ClassifierExcelReader classifierExcelReader, + FacadeTypeExcelReader facadeTypeExcelReader, + SystemPluginConfig systemPluginConfig, + ReportV reportV, + IOpenFileDialogService openFileDialogService, + IMessageBoxService messageBoxService) { + + _pluginConfig = pluginConfig; + _revitRepository = revitRepository; + _localizationService = localizationService; + + _workGroupCode = workGroupCode; + _materialParamSetter = materialParamSetter; + _materialReportService = materialReportService; + _classifierExcelReader = classifierExcelReader; + _facadeTypeExcelReader = facadeTypeExcelReader; + _systemPluginConfig = systemPluginConfig; + _reportV = reportV; + + MessageBoxService = messageBoxService ?? throw new ArgumentNullException(nameof(messageBoxService)); + OpenFileDialogService = openFileDialogService ?? throw new ArgumentNullException(nameof(openFileDialogService)); + + RereadClassifierExcelCommand = RelayCommand.Create(RereadClassifierExcel); + LoadViewCommand = RelayCommand.Create(LoadView); + AcceptViewCommand = RelayCommand.Create(AcceptView, CanAcceptView); + } + + public ICommand RereadClassifierExcelCommand { get; } + public ICommand LoadViewCommand { get; } + public ICommand AcceptViewCommand { get; } + + public IOpenFileDialogService OpenFileDialogService { get; } + public IMessageBoxService MessageBoxService { get; } + + protected List CurrentClassifierWorks { + get => _currentClassifierWorks; + private set => RaiseAndSetIfChanged(ref _currentClassifierWorks, value); + } + + protected List MaterialInPj { + get => _materialInPj; + set => RaiseAndSetIfChanged(ref _materialInPj, value); + } + + public string ExcelClassifierPath { + get => _excelClassifierPath; + set => RaiseAndSetIfChanged(ref _excelClassifierPath, value); + } + + public string ErrorText { + get => _errorText; + set => RaiseAndSetIfChanged(ref _errorText, value); + } + + private void RereadClassifierExcel() { + string classifierDirectory = _systemPluginConfig.ClassifierDirectoryPath; + if(Directory.Exists(classifierDirectory)) { + OpenFileDialogService.InitialDirectory = classifierDirectory; + } + + OpenFileDialogService.Title = _localizationService.GetLocalizedString("MainWindow.SelectClassifierFile"); + if(OpenFileDialogService.ShowDialog()) { + // Если пользователь выбрал файл при выборе файла + ExcelClassifierPath = OpenFileDialogService.File.FullName; + } else { + // Если пользователь не выбрал файл + MessageBoxService.Show(_localizationService.GetLocalizedString("MainWindow.NoClassifierFileSelected")); + return; + } + CurrentClassifierWorks = _classifierExcelReader.Read(ExcelClassifierPath); + if(CurrentClassifierWorks is null || CurrentClassifierWorks.Count == 0) { + MessageBoxService.Show(_localizationService.GetLocalizedString("MainWindow.NoClassifierWorksFound")); + } + } + + protected void ReadClassifierExcel() { + if(string.IsNullOrEmpty(ExcelClassifierPath) || !File.Exists(ExcelClassifierPath)) { + return; + } + + CurrentClassifierWorks = _classifierExcelReader.Read(ExcelClassifierPath); + if(CurrentClassifierWorks is null || CurrentClassifierWorks.Count == 0) { + MessageBoxService.Show(_localizationService.GetLocalizedString("MainWindow.NoClassifierWorksFound")); + } + } + + protected void GetMaterials() { + MaterialInPj = _revitRepository.GetElementMaterials(); + if(MaterialInPj is null || MaterialInPj.Count == 0) { + MessageBoxService.Show(_localizationService.GetLocalizedString("MainWindow.NoMaterialsFound")); + } + } + + protected abstract void LoadView(); + protected abstract void AcceptView(); + protected abstract bool CanAcceptView(); + + /// + /// Показывает окно отчёта, если по итогам обработки есть собранные записи. + /// + protected void ShowReport() { + if(_materialReportService.Items.Count == 0) { + return; + } + + if(_reportV.DataContext is ReportVM reportVM) { + reportVM.UpdateReportData(); + } + _reportV.ShowDialog(); + } + + protected HashSet GetActiveCodes() { + return GetType() + .GetProperties() + .Where(p => p.Name.StartsWith("WorkWith") + && p.PropertyType == typeof(bool) + && (bool) p.GetValue(this)!) + .Select(p => typeof(WorkGroupCode).GetProperty(p.Name.Replace("WorkWith", ""))) + .Where(p => p != null) + .Select(p => p!.GetValue(_workGroupCode)!.ToString()!) + .ToHashSet(); + } +} diff --git a/src/RevitClassifierParameters/ViewModels/ReportVM.cs b/src/RevitClassifierParameters/ViewModels/ReportVM.cs new file mode 100644 index 000000000..16ee06a01 --- /dev/null +++ b/src/RevitClassifierParameters/ViewModels/ReportVM.cs @@ -0,0 +1,47 @@ +using System.Collections.ObjectModel; +using System.Linq; + +using dosymep.WPF.ViewModels; + +using RevitClassifierParameters.Models.MaterialClassifier; + +namespace RevitClassifierParameters.ViewModels; + +internal class ReportVM : BaseViewModel { + private readonly MaterialReportService _materialReportService; + + private ObservableCollection _reportItems = []; + + public ReportVM(MaterialReportService materialReportService) { + _materialReportService = materialReportService; + } + + public ObservableCollection ReportItems { + get => _reportItems; + set => RaiseAndSetIfChanged(ref _reportItems, value); + } + + /// + /// Заполняет отчёт из собранных записей. + /// Порядок групп повторяет Python-плагин: ошибки, изменённые, без изменений, + /// без кода работы, ненайденный код. Внутри группы — сортировка по коду работы. + /// + public void UpdateReportData() { + var ordered = _materialReportService.Items + .OrderBy(GetStatusOrder) + .ThenBy(item => item.WorkCode); + + ReportItems = new ObservableCollection(ordered); + } + + private static int GetStatusOrder(MaterialReportItem item) { + return item.Status switch { + MaterialReportStatus.Error => 0, + MaterialReportStatus.Edited => 1, + MaterialReportStatus.NotEdited => 2, + MaterialReportStatus.NoWorkCode => 3, + MaterialReportStatus.ClassifierCodeNotFound => 4, + _ => 5 + }; + } +} diff --git a/src/RevitClassifierParameters/Views/ArParameterClassifierV.xaml b/src/RevitClassifierParameters/Views/ArParameterClassifierV.xaml new file mode 100644 index 000000000..9800522ca --- /dev/null +++ b/src/RevitClassifierParameters/Views/ArParameterClassifierV.xaml @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/RevitClassifierParameters/Views/ArParameterClassifierV.xaml.cs b/src/RevitClassifierParameters/Views/ArParameterClassifierV.xaml.cs new file mode 100644 index 000000000..3941375c4 --- /dev/null +++ b/src/RevitClassifierParameters/Views/ArParameterClassifierV.xaml.cs @@ -0,0 +1,49 @@ +using System.Windows; + +using dosymep.SimpleServices; + +namespace RevitClassifierParameters.Views; + +/// +/// Класс главного окна плагина. +/// +public partial class ArParameterClassifierV { + /// + /// Иницализирует главное окно плагина. + /// + public ArParameterClassifierV( + ILoggerService loggerService, + ISerializationService serializationService, + ILanguageService languageService, ILocalizationService localizationService, + IUIThemeService uiThemeService, IUIThemeUpdaterService themeUpdaterService) + : base(loggerService, + serializationService, + languageService, localizationService, + uiThemeService, themeUpdaterService) { + InitializeComponent(); + } + + /// + /// Наименование плагина. + /// + /// + /// Используется для сохранения положения окна. + /// + public override string PluginName => nameof(RevitClassifierParameters); + + /// + /// Наименование файла конфигурации. + /// + /// + /// Используется для сохранения положения окна. + /// + public override string ProjectConfigName => nameof(ArParameterClassifierV); + + private void ButtonOk_Click(object sender, RoutedEventArgs e) { + DialogResult = true; + } + + private void ButtonCancel_Click(object sender, RoutedEventArgs e) { + DialogResult = false; + } +} diff --git a/src/RevitClassifierParameters/Views/CustomStyles.xaml b/src/RevitClassifierParameters/Views/CustomStyles.xaml new file mode 100644 index 000000000..30bde8705 --- /dev/null +++ b/src/RevitClassifierParameters/Views/CustomStyles.xaml @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RevitClassifierParameters/Views/ReportV.xaml b/src/RevitClassifierParameters/Views/ReportV.xaml new file mode 100644 index 000000000..2b54e2964 --- /dev/null +++ b/src/RevitClassifierParameters/Views/ReportV.xaml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/RevitClassifierParameters/Views/ReportV.xaml.cs b/src/RevitClassifierParameters/Views/ReportV.xaml.cs new file mode 100644 index 000000000..e1531d243 --- /dev/null +++ b/src/RevitClassifierParameters/Views/ReportV.xaml.cs @@ -0,0 +1,45 @@ +using System.Windows; + +using dosymep.SimpleServices; + +namespace RevitClassifierParameters.Views; + +/// +/// Класс окна отчёта плагина. +/// +public partial class ReportV { + /// + /// Инициализирует окно отчёта плагина. + /// + public ReportV( + ILoggerService loggerService, + ISerializationService serializationService, + ILanguageService languageService, ILocalizationService localizationService, + IUIThemeService uiThemeService, IUIThemeUpdaterService themeUpdaterService) + : base(loggerService, + serializationService, + languageService, localizationService, + uiThemeService, themeUpdaterService) { + InitializeComponent(); + } + + /// + /// Наименование плагина. + /// + /// + /// Используется для сохранения положения окна. + /// + public override string PluginName => nameof(RevitClassifierParameters); + + /// + /// Наименование файла конфигурации. + /// + /// + /// Используется для сохранения положения окна. + /// + public override string ProjectConfigName => nameof(ReportV); + + private void ButtonForHide_Click(object sender, RoutedEventArgs e) { + Hide(); + } +} diff --git a/src/RevitClassifierParameters/assets/Localization/Language.en-US.xaml b/src/RevitClassifierParameters/assets/Localization/Language.en-US.xaml new file mode 100644 index 000000000..74295de2a --- /dev/null +++ b/src/RevitClassifierParameters/assets/Localization/Language.en-US.xaml @@ -0,0 +1,66 @@ + + + Parameters BQW AR + + OK + Cancel + Select + + Select classifier file + Select facade type file + + File with classifier codes + Select an up-to-date excel file with classifier codes for the project + File path not set + + Masonry + Processes materials with codes starting with г02.05 + + Roof + Processes materials with codes starting with г02.06 + + Facades + Processes materials with codes starting with г02.07 + + Fill in facade type + Fill in the facade type into the specified parameter + File with facade type filling rules + Parameter for facade type + + Classifier file is not selected! + No works found in the Classifier! + No materials found! + Facade type rules file is not selected! + No facade types found in the file! + + Classifier file is not selected + No works found in the Classifier + Facade type file is not selected + No facade types found in the file + Parameter for facade types is not selected + + No materials found for elements in the project! + No elements found in the project! + + Classifier sheet is empty or contains no data. + Facade type rules file is empty or contains no data. + + Error + + Filling classifier parameters + Filling facade type + + EDITED + NOT CHANGED + NO WORK CODE + CODE NOT FOUND + WRITE ERROR + + Plugin execution report + Status + Work code + Material name + Close + diff --git a/src/RevitClassifierParameters/assets/Localization/Language.ru-RU.xaml b/src/RevitClassifierParameters/assets/Localization/Language.ru-RU.xaml new file mode 100644 index 000000000..48eabef49 --- /dev/null +++ b/src/RevitClassifierParameters/assets/Localization/Language.ru-RU.xaml @@ -0,0 +1,66 @@ + + + Параметры ВОР АР + + ОК + Отмена + Выбрать + + Выберите файл классификатора + Выберите файл типов фасадов + + Файл с кодами классификатора + Необходимо выбрать актуальный для проекта excel-файл с кодами классификатора + Путь к файлу не указан + + Кладка + Учитывает материалы с кодами, которые начинаются на г02.05 + + Кровля + Учитывает материалы с кодами, которые начинаются на г02.06 + + Фасады + Учитывает материалы с кодами, которые начинаются на г02.07 + + Заполнить тип фасада + Заполнить тип фасада в заданный параметр + Файл с правилами заполнения типа фасада + Параметр для типа фасада + + Не выбран файл Классификатора! + Не найдены работы в Классификаторе! + Не найдено материалов! + Не выбран файл правил заполнения типа фасада! + Не найдены типы фасадов в файле! + + Не выбран файл классификатора + Не найдены работы в Классификаторе + Не выбран файл типов фасадов + Не найдены типы фасадов в файле + Не выбран параметр для типов фасадов + + Не найдено материалов у элементов в проекте! + Не найдено элементов в проекте! + + Лист классификатора пуст или не содержит данных. + Файл правил заполнения типа фасада пуст или не содержит данных. + + Ошибка + + Заполнение параметров классификатора + Заполнение типа фасада + + ИЗМЕНЕН + БЕЗ ИЗМЕНЕНИЙ + НЕТ КОДА РАБОТЫ + НЕ НАЙДЕН КОД + ОШИБКА ПРИ ЗАПИСИ + + Отчет работы плагина + Статус + Код работы + Имя материала + Закрыть +