diff --git a/.github/workflows/publish.RevitSplitMepCurve.yml b/.github/workflows/publish.RevitSplitMepCurve.yml new file mode 100644 index 000000000..b36efc605 --- /dev/null +++ b/.github/workflows/publish.RevitSplitMepCurve.yml @@ -0,0 +1,33 @@ +name: publish RevitSplitMepCurve + +on: + workflow_dispatch: + pull_request: + types: [ closed, synchronize, review_requested ] + branches: [ main, master ] + paths: + - '**RevitSplitMepCurve**.cs' + - '**RevitSplitMepCurve**.xaml' + +env: + plugin-name: "RevitSplitMepCurve" + +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 + + - 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.RevitSplitMepCurve.json b/.nuke/parameters.RevitSplitMepCurve.json new file mode 100644 index 000000000..82a6b2871 --- /dev/null +++ b/.nuke/parameters.RevitSplitMepCurve.json @@ -0,0 +1,15 @@ +{ + "$schema": "./build.schema.json", + "Solution": "RevitPlugins.slnx", + "PluginName": "RevitSplitMepCurve", + "PublishDirectory": "01.BIM.extension\\BIM.tab\\bin", + "RevitVersions": [ + "Rv2022", + "Rv2023", + "Rv2024" + ], + "IconUrl": "https://icons8.com/icon/22945/cut-paper", + "BundleName": "Разделить по уровню СМР", + "BundleType": "InvokeButton", + "BundleOutput": "01.BIM.extension\\BIM.tab\\СМР.panel\\СМР.pulldown" +} diff --git a/RevitPlugins.slnx b/RevitPlugins.slnx index 6ff59a6f3..316538401 100644 --- a/RevitPlugins.slnx +++ b/RevitPlugins.slnx @@ -209,6 +209,9 @@ + + + diff --git a/src/RevitSplitMepCurve/Models/ConnectorConfig.cs b/src/RevitSplitMepCurve/Models/ConnectorConfig.cs new file mode 100644 index 000000000..306c03518 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/ConnectorConfig.cs @@ -0,0 +1,25 @@ +using System; + +using Autodesk.Revit.DB; + +namespace RevitSplitMepCurve.Models; + +internal class ConnectorConfig { + public string FamilyName { get; set; } = string.Empty; + + public string SymbolName { get; set; } = string.Empty; + + public bool Equals(FamilySymbol s) { + if(s is null) { + return false; + } + + if(FamilyName is null + || SymbolName is null) { + return false; + } + + return SymbolName.Equals(s.Name, StringComparison.CurrentCultureIgnoreCase) + && FamilyName.Equals(s.FamilyName, StringComparison.CurrentCultureIgnoreCase); + } +} diff --git a/src/RevitSplitMepCurve/Models/Enums/MepClass.cs b/src/RevitSplitMepCurve/Models/Enums/MepClass.cs new file mode 100644 index 000000000..a83a5a420 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Enums/MepClass.cs @@ -0,0 +1,6 @@ +namespace RevitSplitMepCurve.Models.Enums; + +internal enum MepClass { + Pipes, + Ducts +} diff --git a/src/RevitSplitMepCurve/Models/Enums/SelectionMode.cs b/src/RevitSplitMepCurve/Models/Enums/SelectionMode.cs new file mode 100644 index 000000000..7be1f6073 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Enums/SelectionMode.cs @@ -0,0 +1,7 @@ +namespace RevitSplitMepCurve.Models.Enums; + +internal enum SelectionMode { + SelectedElements, + ActiveView, + ActiveDocument +} diff --git a/src/RevitSplitMepCurve/Models/Errors/ErrorModel.cs b/src/RevitSplitMepCurve/Models/Errors/ErrorModel.cs new file mode 100644 index 000000000..54619fe41 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Errors/ErrorModel.cs @@ -0,0 +1,54 @@ +using System; + +using Autodesk.Revit.DB; + +namespace RevitSplitMepCurve.Models.Errors; + +internal class ErrorModel : IEquatable { + private readonly ElementId _id; + + public ErrorModel(Element element, string message) { + if(string.IsNullOrWhiteSpace(message)) { + throw new ArgumentException(nameof(message)); + } + Message = message; + Element = element ?? throw new ArgumentNullException(nameof(element)); + _id = Element.Id; + } + + public string Message { get; } + + public Element Element { get; } + + public bool Equals(ErrorModel other) { + if(other is null) { + return false; + } + + if(ReferenceEquals(this, other)) { + return true; + } + + return Equals(_id, other._id); + } + + public override bool Equals(object obj) { + if(obj is null) { + return false; + } + + if(ReferenceEquals(this, obj)) { + return true; + } + + if(obj.GetType() != GetType()) { + return false; + } + + return Equals((ErrorModel) obj); + } + + public override int GetHashCode() { + return _id.GetHashCode(); + } +} diff --git a/src/RevitSplitMepCurve/Models/Exceptions/CannotCreateConnectorException.cs b/src/RevitSplitMepCurve/Models/Exceptions/CannotCreateConnectorException.cs new file mode 100644 index 000000000..a24e5cec6 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Exceptions/CannotCreateConnectorException.cs @@ -0,0 +1,16 @@ +using System; + +namespace RevitSplitMepCurve.Models.Exceptions; + +/// +/// Исключение, когда не удалось создать коннектор +/// +internal class CannotCreateConnectorException : Exception { + public CannotCreateConnectorException() + : base() { + } + + public CannotCreateConnectorException(string msg) + : base(msg) { + } +} diff --git a/src/RevitSplitMepCurve/Models/Exceptions/CannotGetConnectorSymbolException.cs b/src/RevitSplitMepCurve/Models/Exceptions/CannotGetConnectorSymbolException.cs new file mode 100644 index 000000000..6a6e61889 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Exceptions/CannotGetConnectorSymbolException.cs @@ -0,0 +1,16 @@ +using System; + +namespace RevitSplitMepCurve.Models.Exceptions; + +/// +/// Исключение, когда не удалось получить типоразмер соединителя для вставки +/// +internal class CannotGetConnectorSymbolException : Exception { + public CannotGetConnectorSymbolException() + : base() { + } + + public CannotGetConnectorSymbolException(string msg) + : base(msg) { + } +} diff --git a/src/RevitSplitMepCurve/Models/PluginConfig.cs b/src/RevitSplitMepCurve/Models/PluginConfig.cs new file mode 100644 index 000000000..131f4a9d1 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/PluginConfig.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +using dosymep.Bim4Everyone; +using dosymep.Bim4Everyone.ProjectConfigs; + +using pyRevitLabs.Json; + +using RevitSplitMepCurve.Models.Enums; + +namespace RevitSplitMepCurve.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(RevitSplitMepCurve)) + .SetRevitVersion(ModuleEnvironment.RevitVersion) + .SetProjectConfigName(nameof(PluginConfig) + ".json") + .Build(); + } +} + +internal class RevitSettings : ProjectSettings { + public const MepClass DefaultMepClass = MepClass.Pipes; + public const SelectionMode DefaultSelectionMode = SelectionMode.ActiveView; + + public override string ProjectName { get; set; } + + public MepClass SelectedMepClass { get; set; } = DefaultMepClass; + + public SelectionMode SelectedMode { get; set; } = DefaultSelectionMode; + + public ConnectorConfig RoundConnector { get; set; } = new ConnectorConfig(); + + public ConnectorConfig RectangleConnector { get; set; } = new ConnectorConfig(); + + /// Имена уровней, исключённых пользователем. + public List UncheckedLevelNames { get; set; } = []; + + public bool ShowSplitErrors { get; set; } = true; +} diff --git a/src/RevitSplitMepCurve/Models/RevitRepository.cs b/src/RevitSplitMepCurve/Models/RevitRepository.cs new file mode 100644 index 000000000..fccba49f1 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/RevitRepository.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.ApplicationServices; +using Autodesk.Revit.DB; +using Autodesk.Revit.UI; + +using dosymep.Revit; + +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Splittable; + +namespace RevitSplitMepCurve.Models; + +internal class RevitRepository { + public RevitRepository(UIApplication uiApplication) { + UIApplication = uiApplication ?? throw new ArgumentNullException(nameof(uiApplication)); + } + + public UIApplication UIApplication { get; } + + public UIDocument ActiveUIDocument => UIApplication.ActiveUIDocument; + + public Application Application => UIApplication.Application; + + public Document Document => ActiveUIDocument.Document; + + public ICollection GetLevels() { + return new FilteredElementCollector(Document) + .WhereElementIsNotElementType() + .OfClass(typeof(Level)) + .OfType() + .ToArray(); + } + + public ICollection GetConnectorSymbols(BuiltInCategory category, ConnectorProfileType shape) { + return GetConnectorSymbols(category) + .GroupBy(s => s.Family.Id) + .Where(g => IsConnectorFamilyWithShape(g.First().Family, shape)) + .SelectMany(g => g) + .ToArray(); + } + + /// + /// Возвращает все типоразмеры семейств с типом детали "Соединение" + /// + /// Категория типоразмеров семейств + public ICollection GetConnectorSymbols(BuiltInCategory category) { + return new FilteredElementCollector(Document) + .WhereElementIsElementType() + .OfClass(typeof(FamilySymbol)) + .OfCategory(category) + .OfType() + .Where(s => s.Family.GetParamValueOrDefault(BuiltInParameter.FAMILY_CONTENT_PART_TYPE) + == (int) PartType.Union) + .ToArray(); + } + + /// Все элементы заданной категории во всём документе. + public ICollection GetElements(BuiltInCategory category) where T : MEPCurve { + return new FilteredElementCollector(Document) + .WhereElementIsNotElementType() + .OfCategory(category) + .OfClass(typeof(T)) + .OfType() + .ToArray(); + } + + /// Все элементы заданной категории на активном виде. + public ICollection GetActiveViewElements(BuiltInCategory category) where T : MEPCurve { + return new FilteredElementCollector(Document, Document.ActiveView.Id) + .WhereElementIsNotElementType() + .OfCategory(category) + .OfClass(typeof(T)) + .OfType() + .ToArray(); + } + + /// Текущий выделенный набор пользователя, отфильтрованный по категории. + public ICollection GetSelectedElements(BuiltInCategory category) where T : MEPCurve { + return new FilteredElementCollector(Document, ActiveUIDocument.Selection.GetElementIds()) + .WhereElementIsNotElementType() + .OfCategory(category) + .OfClass(typeof(T)) + .OfType() + .ToArray(); + } + + /// true, если у пользователя выбран хотя бы один элемент. + public bool HasSelectedElements() { + return ActiveUIDocument.Selection.GetElementIds().Count > 0; + } + + /// Группы уровней с одинаковыми отметками + public ICollection> GetDuplicateLevels() { + var levels = GetLevels(); + return GroupIntersectingLevels(levels); + } + + public ICollection GetDisplacementElements() { + return new FilteredElementCollector(Document) + .WhereElementIsNotElementType() + .OfClass(typeof(DisplacementElement)) + .OfType() + .ToArray(); + } + + public bool IsSyncRequired(ICollection elements) { + if(!Document.IsWorkshared) { + return false; + } + foreach(var splittable in elements) { + var id = splittable.Element.Id; + var checkoutStatus = WorksharingUtils.GetCheckoutStatus(Document, id); + if(checkoutStatus == CheckoutStatus.OwnedByOtherUser) { + return true; + } + var updateStatus = WorksharingUtils.GetModelUpdatesStatus(Document, id); + if(updateStatus == ModelUpdatesStatus.UpdatedInCentral) { + return true; + } + } + return false; + } + + public void ShowElements(params Element[] elements) { + if(elements is null + || elements.Length == 0) { + return; + } + + ElementId[] elementIds = elements + .Select(item => item.Id) + .ToArray(); + + ActiveUIDocument.ShowElements(elementIds); + ActiveUIDocument.Selection.SetElementIds(elementIds); + } + + /// + /// Проверяет, что семейство - соединитель с заданной формой + /// + /// Семейство + /// Форма соединителя + /// True, если в семействе ровно 2 соединителя заданной формы, иначе - False + private bool IsConnectorFamilyWithShape(Family family, ConnectorProfileType shape) { + using var fDoc = Document.EditFamily(family); + var connectors = GetConnectors(fDoc); + bool result = connectors.Count == 2 && connectors.All(c => c.Shape == shape); + fDoc.Close(false); + return result; + } + + private ICollection GetConnectors(Document familyDocument) { + return new FilteredElementCollector(familyDocument) + .WhereElementIsNotElementType() + .OfClass(typeof(ConnectorElement)) + .OfType() + .ToArray(); + } + + /// + /// Группирует уровни, которые пересекаются по высоте с учетом допуска ревита по длине линий + /// + private ICollection> GroupIntersectingLevels(ICollection levels) { + if(levels == null + || !levels.Any()) { + return []; + } + + double tolerance = Application.ShortCurveTolerance; + + var sortedLevels = levels.OrderBy(l => l.Elevation).ToArray(); + var groups = new List>(); + var currentGroup = new List { sortedLevels[0] }; + + for(int i = 1; i < sortedLevels.Length; i++) { + var prevLevel = sortedLevels[i - 1]; + var currentLevel = sortedLevels[i]; + + double prevTop = prevLevel.Elevation + tolerance; + double currentBottom = currentLevel.Elevation - tolerance; + + if(currentBottom <= prevTop) { + currentGroup.Add(currentLevel); + } else { + groups.Add(currentGroup); + currentGroup = [currentLevel]; + } + } + + groups.Add(currentGroup); + + return groups; + } +} diff --git a/src/RevitSplitMepCurve/Models/Settings/ISplitSettings.cs b/src/RevitSplitMepCurve/Models/Settings/ISplitSettings.cs new file mode 100644 index 000000000..4057f486a --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Settings/ISplitSettings.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +using Autodesk.Revit.DB; + +namespace RevitSplitMepCurve.Models.Settings; + +internal interface ISplitSettings { + /// Соединитель для труб и круглых воздуховодов. + FamilySymbol ConnectorRoundSymbol { get; } + + /// Соединитель для прямоугольных воздуховодов. + FamilySymbol ConnectorRectangleSymbol { get; } + + /// Уровни, на которых надо делить. + ICollection Levels { get; } +} diff --git a/src/RevitSplitMepCurve/Models/Settings/SplitSettings.cs b/src/RevitSplitMepCurve/Models/Settings/SplitSettings.cs new file mode 100644 index 000000000..4b69828fe --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Settings/SplitSettings.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +using Autodesk.Revit.DB; + +namespace RevitSplitMepCurve.Models.Settings; + +internal class SplitSettings : ISplitSettings { + public SplitSettings(FamilySymbol round, FamilySymbol rectangle, ICollection levels) { + ConnectorRoundSymbol = round; + ConnectorRectangleSymbol = rectangle; + Levels = levels ?? []; + } + + public FamilySymbol ConnectorRoundSymbol { get; } + + public FamilySymbol ConnectorRectangleSymbol { get; } + + public ICollection Levels { get; } +} diff --git a/src/RevitSplitMepCurve/Models/Splittable/SplitResult.cs b/src/RevitSplitMepCurve/Models/Splittable/SplitResult.cs new file mode 100644 index 000000000..c09544837 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Splittable/SplitResult.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; + +using dosymep.Revit; + +namespace RevitSplitMepCurve.Models.Splittable; + +internal class SplitResult { + public SplitResult( + MEPCurve original, + ICollection newSegments, + ICollection insertedConnectors, + ICollection displacementElements) { + Original = original ?? throw new ArgumentNullException(nameof(original)); + NewSegments = newSegments ?? []; + InsertedConnectors = insertedConnectors ?? []; + DisplacementElements = displacementElements ?? []; + } + + public MEPCurve Original { get; } + + public ICollection NewSegments { get; } + + public ICollection InsertedConnectors { get; } + + public ICollection DisplacementElements { get; } + + /// + /// Обновляет рабочий набор у новых сегментов и коннекторов, + /// добавляет их во все DisplacementElement, в которые входил Original. + /// + public void UpdateSegments() { + var sourceWorksetId = Original.WorksetId; + Element[] newEls = [..NewSegments, .. InsertedConnectors]; + + foreach(var element in newEls) { + SetWorksetId(element, sourceWorksetId); + } + + foreach(var de in DisplacementElements) { + UpdateDisplacementElement(de, newEls); + } + } + + private void SetWorksetId(Element element, WorksetId worksetId) { + if(!element.Document.IsWorkshared) { + return; + } + + element.SetParamValue(BuiltInParameter.ELEM_PARTITION_PARAM, worksetId.IntegerValue); + } + + private void UpdateDisplacementElement(DisplacementElement de, ICollection newEls) { + var view = (View) de.Document.GetElement(de.OwnerViewId); + HashSet ids = [ + ..de.GetDisplacedElementIds(), + ..newEls.Where(el => DisplacementElement.IsAllowedAsDisplacedElement(el) + && !DisplacementElement.IsElementDisplacedInView(view, el.Id)) + .Select(el => el.Id) + ]; + + de.SetDisplacedElementIds(ids); + } +} diff --git a/src/RevitSplitMepCurve/Models/Splittable/SplittableDuct.cs b/src/RevitSplitMepCurve/Models/Splittable/SplittableDuct.cs new file mode 100644 index 000000000..f288240ba --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Splittable/SplittableDuct.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +using Autodesk.Revit.DB; +using Autodesk.Revit.DB.Mechanical; + +using RevitSplitMepCurve.Models.Exceptions; +using RevitSplitMepCurve.Models.Settings; + +namespace RevitSplitMepCurve.Models.Splittable; + +internal class SplittableDuct : SplittableElement { + private readonly Duct _duct; + + public SplittableDuct(Duct duct, ICollection displacementElements) + : base(duct, displacementElements) { + _duct = duct; + } + + public override SplitResult Split(ISplitSettings settings) { + var doc = _duct.Document; + var intersections = GetIntersections(settings.Levels); + + var newSegments = new List(); + var insertedConnectors = new List(); + + for(int i = 0; i < intersections.Count; i++) { + var point = intersections[i]; + var newId = MechanicalUtils.BreakCurve(doc, _duct.Id, point); + var newDuct = (Duct) doc.GetElement(newId); + newSegments.Add(newDuct); + + FamilySymbol connectorSymbol; + if(_duct.DuctType.Shape == ConnectorProfileType.Round) { + connectorSymbol = settings.ConnectorRoundSymbol; + } else if(_duct.DuctType.Shape == ConnectorProfileType.Rectangular) { + connectorSymbol = settings.ConnectorRectangleSymbol; + } else { + throw new CannotGetConnectorSymbolException(); + } + + var connector1 = GetClosestConnector(_duct, point); + var connector2 = GetClosestConnector(newDuct, point); + var fitting = InsertConnector( + connectorSymbol, + newDuct.DuctType, + connector1, + connector2); + insertedConnectors.Add(fitting); + } + + return new SplitResult(_duct, newSegments, insertedConnectors, DisplacementElements); + } +} diff --git a/src/RevitSplitMepCurve/Models/Splittable/SplittableElement.cs b/src/RevitSplitMepCurve/Models/Splittable/SplittableElement.cs new file mode 100644 index 000000000..054c65897 --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Splittable/SplittableElement.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; + +using RevitSplitMepCurve.Models.Exceptions; +using RevitSplitMepCurve.Models.Settings; + +namespace RevitSplitMepCurve.Models.Splittable; + +internal abstract class SplittableElement { + protected SplittableElement(MEPCurve element, ICollection displacementElements) { + Element = element ?? throw new ArgumentNullException(nameof(element)); + DisplacementElements = displacementElements ?? []; + } + + public MEPCurve Element { get; } + + /// Все DisplacementElement-ы, в которые входит Element. + public ICollection DisplacementElements { get; } + + /// true, если кривая пересекает хотя бы один уровень. + public bool CanBeSplitted(ICollection levels) { + if(levels.Count == 0) { + return false; + } + + return levels.Any(CanBeSplitted); + } + + /// Делит элемент и возвращает результат. + public abstract SplitResult Split(ISplitSettings settings); + + protected (XYZ p0, XYZ p1) GetEndPoints() { + var curve = ((LocationCurve) Element.Location).Curve; + var p0 = curve.GetEndPoint(0); + var p1 = curve.GetEndPoint(1); + return (p0, p1); + } + + protected Connector GetClosestConnector(MEPCurve mepEl, XYZ point) { + return mepEl.ConnectorManager.Connectors + .OfType() + .OrderBy(c => c.Origin.DistanceTo(point)) + .First(); + } + + /// + /// Точки, по которым надо разбивать осевую линию ВИС элемента. + /// Когда элемент ВИС делится - новый кусок будет создаваться со стороны GetEndPoint(0) точки линии. + /// Поэтому при последовательном многократном делении одного и того же элемента надо делить в направлении от 0 к 1. + protected IList GetIntersections(ICollection levels) { + (var p0, var p1) = GetEndPoints(); + Level[] sortedLevels; + XYZ curveMin, curveMax; + if(p0.Z <= p1.Z) { + sortedLevels = levels.OrderBy(l => l.Elevation).ToArray(); + (curveMin, curveMax) = (p0, p1); + } else { + sortedLevels = levels.OrderByDescending(l => l.Elevation).ToArray(); + (curveMin, curveMax) = (p1, p0); + } + + var results = new List(); + for(int i = 0; i < sortedLevels.Length; i++) { + var level = sortedLevels[i]; + if(!CanBeSplitted(level)) { + continue; + } + + double curveLengthZ = curveMax.Z - curveMin.Z; + double proportion = (level.Elevation - curveMin.Z) / curveLengthZ; + var point = curveMin + (curveMax - curveMin) * proportion; + results.Add(point); + } + + return results; + } + + protected FamilyInstance InsertConnector( + FamilySymbol connectorSymbol, + MEPCurveType mepType, + Connector mepEl1, + Connector mepEl2) { + const RoutingPreferenceRuleGroupType group = RoutingPreferenceRuleGroupType.Unions; + var tempRule = new RoutingPreferenceRule(connectorSymbol.Id, "TEMP_UNION_RULE"); + + mepType.RoutingPreferenceManager.AddRule(group, tempRule, 0); + FamilyInstance createdConnector; + try { + createdConnector = mepType.Document.Create.NewUnionFitting(mepEl1, mepEl2); + } catch(Autodesk.Revit.Exceptions.ArgumentException) { + throw new CannotCreateConnectorException(); + } catch(Autodesk.Revit.Exceptions.InvalidOperationException) { + throw new CannotCreateConnectorException(); + } + + mepType.RoutingPreferenceManager.RemoveRule(group, 0); + return createdConnector; + } + + private bool CanBeSplitted(Level level) { + double tolerance = Element.Document.Application.ShortCurveTolerance; + (var p0, var p1) = GetEndPoints(); + double elMax = Math.Max(p0.Z, p1.Z); + double elMin = Math.Min(p0.Z, p1.Z); + + double levelMax = level.Elevation + tolerance; + double levelMin = level.Elevation - tolerance; + return elMin < levelMin && levelMax < elMax; + } +} diff --git a/src/RevitSplitMepCurve/Models/Splittable/SplittablePipe.cs b/src/RevitSplitMepCurve/Models/Splittable/SplittablePipe.cs new file mode 100644 index 000000000..40a9a521e --- /dev/null +++ b/src/RevitSplitMepCurve/Models/Splittable/SplittablePipe.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +using Autodesk.Revit.DB; +using Autodesk.Revit.DB.Plumbing; + +using RevitSplitMepCurve.Models.Exceptions; +using RevitSplitMepCurve.Models.Settings; + +namespace RevitSplitMepCurve.Models.Splittable; + +internal class SplittablePipe : SplittableElement { + private readonly Pipe _pipe; + + public SplittablePipe(Pipe pipe, ICollection displacementElements) + : base(pipe, displacementElements) { + _pipe = pipe; + } + + public override SplitResult Split(ISplitSettings settings) { + var doc = _pipe.Document; + var intersections = GetIntersections(settings.Levels); + + var newSegments = new List(); + var insertedConnectors = new List(); + + for(int i = 0; i < intersections.Count; i++) { + var point = intersections[i]; + var newId = PlumbingUtils.BreakCurve(doc, _pipe.Id, point); + var newPipe = (Pipe) doc.GetElement(newId); + newSegments.Add(newPipe); + + var connectorSymbol = settings.ConnectorRoundSymbol + ?? throw new CannotGetConnectorSymbolException(); + + var connector1 = GetClosestConnector(_pipe, point); + var connector2 = GetClosestConnector(newPipe, point); + var fitting = InsertConnector( + connectorSymbol, + newPipe.PipeType, + connector1, + connector2); + insertedConnectors.Add(fitting); + } + + return new SplitResult(_pipe, newSegments, insertedConnectors, DisplacementElements); + } +} diff --git a/src/RevitSplitMepCurve/README.md b/src/RevitSplitMepCurve/README.md new file mode 100644 index 000000000..78c1517dc --- /dev/null +++ b/src/RevitSplitMepCurve/README.md @@ -0,0 +1,15 @@ +# RevitSplitMepCurve (Разделить по уровню СМР) + +Плагин разделяет линейные элементы заданных категорий по этажам. + +# Сборка проекта + +``` +nuke compile --profile RevitSplitMepCurve +``` + +# Публикация проекта + +``` +nuke publish --profile RevitSplitMepCurve +``` diff --git a/src/RevitSplitMepCurve/Resources/ComboBoxBehavior.cs b/src/RevitSplitMepCurve/Resources/ComboBoxBehavior.cs new file mode 100644 index 000000000..d36883a6b --- /dev/null +++ b/src/RevitSplitMepCurve/Resources/ComboBoxBehavior.cs @@ -0,0 +1,47 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; + +namespace RevitSplitMepCurve.Resources; +internal class ComboBoxBehavior { + public static readonly DependencyProperty OpenDropDownOnTextChangedProperty = + DependencyProperty.RegisterAttached( + "OpenDropDownOnTextChanged", + typeof(bool), + typeof(ComboBoxBehavior), + new PropertyMetadata(false, OnOpenDropDownOnTextChangedChanged)); + + public static bool GetOpenDropDownOnTextChanged(DependencyObject obj) { + return (bool) obj.GetValue(OpenDropDownOnTextChangedProperty); + } + + public static void SetOpenDropDownOnTextChanged(DependencyObject obj, bool value) { + obj.SetValue(OpenDropDownOnTextChangedProperty, value); + } + + private static void OnOpenDropDownOnTextChangedChanged( + DependencyObject d, + DependencyPropertyChangedEventArgs e) { + if(d is ComboBox comboBox) { + if((bool) e.NewValue) { + comboBox.AddHandler( + TextBoxBase.TextChangedEvent, + new TextChangedEventHandler(OnTextChanged)); + } else { + comboBox.RemoveHandler( + TextBoxBase.TextChangedEvent, + new TextChangedEventHandler(OnTextChanged)); + } + } + } + + private static void OnTextChanged(object sender, TextChangedEventArgs e) { + if(sender is ComboBox comboBox + && comboBox.IsEditable + && comboBox.IsKeyboardFocusWithin) { + if(!string.IsNullOrEmpty(comboBox.Text)) { + comboBox.IsDropDownOpen = true; + } + } + } +} diff --git a/src/RevitSplitMepCurve/RevitSplitMepCurve.csproj b/src/RevitSplitMepCurve/RevitSplitMepCurve.csproj new file mode 100644 index 000000000..ae3efaefb --- /dev/null +++ b/src/RevitSplitMepCurve/RevitSplitMepCurve.csproj @@ -0,0 +1,6 @@ + + + true + 12 + + diff --git a/src/RevitSplitMepCurve/RevitSplitMepCurveCommand.cs b/src/RevitSplitMepCurve/RevitSplitMepCurveCommand.cs new file mode 100644 index 000000000..a4c27011b --- /dev/null +++ b/src/RevitSplitMepCurve/RevitSplitMepCurveCommand.cs @@ -0,0 +1,88 @@ +using System.Globalization; +using System.Linq; +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.SimpleServices; +using dosymep.WpfCore.Ninject; +using dosymep.WpfUI.Core.Ninject; + +using Ninject; + +using RevitSplitMepCurve.Models; +using RevitSplitMepCurve.Services.Core; +using RevitSplitMepCurve.Services.Providers; +using RevitSplitMepCurve.ViewModels; +using RevitSplitMepCurve.Views; + +namespace RevitSplitMepCurve; + +[Transaction(TransactionMode.Manual)] +public class RevitSplitMepCurveCommand : BasePluginCommand { + public RevitSplitMepCurveCommand() { + PluginName = "Разделить по уровню СМР"; + } + + protected override void Execute(UIApplication uiApplication) { + using var kernel = uiApplication.CreatePlatformServices(); + + kernel.Bind().ToSelf().InSingletonScope(); + + kernel.Bind() + .ToMethod(c => PluginConfig.GetPluginConfig(c.Kernel.Get())); + + BindCoreServices(kernel); + BindProviders(kernel); + BindWindows(kernel); + + kernel.UseWpfUIThemeUpdater(); + kernel.UseWpfUIMessageBox(); + kernel.UseWpfUIProgressDialog(); + + string assemblyName = Assembly.GetExecutingAssembly().GetName().Name; + kernel.UseWpfLocalization( + $"/{assemblyName};component/assets/localization/language.xaml", + CultureInfo.GetCultureInfo("ru-RU")); + + var localization = kernel.Get(); + var messageBox = kernel.Get(); + var repository = kernel.Get(); + + var levelDubles = repository.GetDuplicateLevels().FirstOrDefault(g => g.Count > 1); + if(levelDubles is not null) { + messageBox.Show( + localization.GetLocalizedString( + "Error.DuplicateLevels", + string.Join("\n", levelDubles.Select(l => l.Name))), + localization.GetLocalizedString("MainWindow.Title"), + MessageBoxButton.OK, + MessageBoxImage.Warning); + return; + } + + Notification(kernel.Get()); + } + + private void BindCoreServices(IKernel kernel) { + kernel.Bind().To().InSingletonScope(); + kernel.Bind().ToSelf().InSingletonScope(); + } + + private void BindProviders(IKernel kernel) { + kernel.Bind().ToSelf().InSingletonScope(); + kernel.Bind().ToSelf().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + } + + private void BindWindows(IKernel kernel) { + kernel.BindMainWindow(); + kernel.BindOtherWindow(); + } +} diff --git a/src/RevitSplitMepCurve/Services/Core/ErrorsService.cs b/src/RevitSplitMepCurve/Services/Core/ErrorsService.cs new file mode 100644 index 000000000..ea99ca9e9 --- /dev/null +++ b/src/RevitSplitMepCurve/Services/Core/ErrorsService.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +using Autodesk.Revit.DB; + +using dosymep.SimpleServices; + +using RevitSplitMepCurve.Models.Errors; + +namespace RevitSplitMepCurve.Services.Core; + +internal class ErrorsService : IErrorsService { + private readonly List _errors; + private readonly ILocalizationService _localizationService; + + public ErrorsService(ILocalizationService localizationService) { + _localizationService = localizationService + ?? throw new ArgumentNullException(nameof(localizationService)); + _errors = []; + } + + public void AddError(ErrorModel error) { + if(error == null) { + throw new ArgumentNullException(nameof(error)); + } + + _errors.Add(error); + } + + public void AddError(Element element, string localizationKey) { + if(element == null) { + throw new ArgumentNullException(nameof(element)); + } + + if(string.IsNullOrWhiteSpace(localizationKey)) { + throw new ArgumentException(nameof(localizationKey)); + } + _errors.Add(new ErrorModel(element, _localizationService.GetLocalizedString(localizationKey))); + } + + public ICollection GetErrors() => _errors; + + public bool ContainsErrors() => _errors.Count > 0; + + public void Clear() => _errors.Clear(); +} diff --git a/src/RevitSplitMepCurve/Services/Core/ErrorsWindowService.cs b/src/RevitSplitMepCurve/Services/Core/ErrorsWindowService.cs new file mode 100644 index 000000000..5c40f4fb7 --- /dev/null +++ b/src/RevitSplitMepCurve/Services/Core/ErrorsWindowService.cs @@ -0,0 +1,20 @@ +using System; + +using Ninject; +using Ninject.Syntax; + +using RevitSplitMepCurve.Views; + +namespace RevitSplitMepCurve.Services.Core; + +internal class ErrorsWindowService { + private readonly IResolutionRoot _root; + + public ErrorsWindowService(IResolutionRoot root) { + _root = root ?? throw new ArgumentNullException(nameof(root)); + } + + public void ShowErrorsWindow() { + _root.Get().Show(); + } +} diff --git a/src/RevitSplitMepCurve/Services/Core/IErrorsService.cs b/src/RevitSplitMepCurve/Services/Core/IErrorsService.cs new file mode 100644 index 000000000..e0738cf6c --- /dev/null +++ b/src/RevitSplitMepCurve/Services/Core/IErrorsService.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +using Autodesk.Revit.DB; + +using RevitSplitMepCurve.Models.Errors; + +namespace RevitSplitMepCurve.Services.Core; + +internal interface IErrorsService { + void AddError(ErrorModel error); + + void AddError(Element element, string localizationKey); + + ICollection GetErrors(); + + bool ContainsErrors(); + + void Clear(); +} diff --git a/src/RevitSplitMepCurve/Services/Providers/DuctsProvider.cs b/src/RevitSplitMepCurve/Services/Providers/DuctsProvider.cs new file mode 100644 index 000000000..1095387ef --- /dev/null +++ b/src/RevitSplitMepCurve/Services/Providers/DuctsProvider.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; +using Autodesk.Revit.DB.Mechanical; + +using RevitSplitMepCurve.Models; +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Splittable; + +namespace RevitSplitMepCurve.Services.Providers; + +internal class DuctsProvider : ElementsProvider { + public DuctsProvider(RevitRepository revitRepository) : base(revitRepository) { + } + + public override MepClass MepClass => MepClass.Ducts; + + protected override ICollection GetSelectedElements() { + return _revitRepository.GetSelectedElements(BuiltInCategory.OST_DuctCurves) + .Where(IsRectangleOrRound) + .ToArray(); + } + + protected override ICollection GetElementsOnActiveView() { + return _revitRepository.GetActiveViewElements(BuiltInCategory.OST_DuctCurves) + .Where(IsRectangleOrRound) + .ToArray(); + } + + protected override ICollection GetElementsInDocument() { + return _revitRepository.GetElements(BuiltInCategory.OST_DuctCurves) + .Where(IsRectangleOrRound) + .ToArray(); + } + + protected override SplittableElement CreateSplittable(MEPCurve curve, ICollection all) { + var filtered = FilterDisplacements(curve, all); + return new SplittableDuct((Duct) curve, filtered); + } + + private bool IsRectangleOrRound(Duct duct) { + return duct.DuctType?.Shape is ConnectorProfileType.Round or ConnectorProfileType.Rectangular; + } +} diff --git a/src/RevitSplitMepCurve/Services/Providers/ElementsProvider.cs b/src/RevitSplitMepCurve/Services/Providers/ElementsProvider.cs new file mode 100644 index 000000000..4382e902c --- /dev/null +++ b/src/RevitSplitMepCurve/Services/Providers/ElementsProvider.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; + +using RevitSplitMepCurve.Models; +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Splittable; + +namespace RevitSplitMepCurve.Services.Providers; + +internal abstract class ElementsProvider : IElementsProvider { + protected readonly RevitRepository _revitRepository; + + protected ElementsProvider(RevitRepository revitRepository) { + _revitRepository = revitRepository ?? throw new ArgumentNullException(nameof(revitRepository)); + } + + public abstract MepClass MepClass { get; } + + public ICollection GetElements(SelectionMode selectionMode) { + ICollection raw = selectionMode switch { + SelectionMode.SelectedElements => GetSelectedElements(), + SelectionMode.ActiveView => GetElementsOnActiveView(), + SelectionMode.ActiveDocument => GetElementsInDocument(), + _ => throw new ArgumentOutOfRangeException(nameof(selectionMode)) + }; + + var displacements = _revitRepository.GetDisplacementElements(); + return [.. raw.Where(IsValid).Select(c => CreateSplittable(c, displacements))]; + } + + protected abstract ICollection GetSelectedElements(); + + protected abstract ICollection GetElementsOnActiveView(); + + protected abstract ICollection GetElementsInDocument(); + + protected abstract SplittableElement CreateSplittable(MEPCurve curve, ICollection all); + + /// + /// Фильтрует наборы смещения, в которых есть заданный элемент ВИС + /// + /// Элемент ВИС + /// Все наборы смещения для проверки + /// Наборы смещения, в которых есть заданный элемент ВИС + protected ICollection FilterDisplacements( + MEPCurve curve, + ICollection all) { + return all.Where(de => de.GetDisplacedElementIds().Contains(curve.Id)).ToArray(); + } + + /// + /// Проверяет, что элемент не короткий и не горизонтальный + /// + private bool IsValid(MEPCurve mepCurve) { + double tolerance = _revitRepository.Application.ShortCurveTolerance; + if(mepCurve.Location is not LocationCurve locationCurve) { + return false; + } + + if(locationCurve.Curve.Length <= tolerance) { + return false; + } + + if(Math.Abs(locationCurve.Curve.GetEndPoint(0).Z - locationCurve.Curve.GetEndParameter(1)) + <= tolerance) { + return false; + } + + return true; + } +} diff --git a/src/RevitSplitMepCurve/Services/Providers/IElementsProvider.cs b/src/RevitSplitMepCurve/Services/Providers/IElementsProvider.cs new file mode 100644 index 000000000..6ee2189e6 --- /dev/null +++ b/src/RevitSplitMepCurve/Services/Providers/IElementsProvider.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Splittable; + +namespace RevitSplitMepCurve.Services.Providers; + +internal interface IElementsProvider { + MepClass MepClass { get; } + + ICollection GetElements(SelectionMode selectionMode); +} diff --git a/src/RevitSplitMepCurve/Services/Providers/PipesProvider.cs b/src/RevitSplitMepCurve/Services/Providers/PipesProvider.cs new file mode 100644 index 000000000..e32ab75a5 --- /dev/null +++ b/src/RevitSplitMepCurve/Services/Providers/PipesProvider.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; +using Autodesk.Revit.DB.Plumbing; + +using RevitSplitMepCurve.Models; +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Splittable; + +namespace RevitSplitMepCurve.Services.Providers; + +internal class PipesProvider : ElementsProvider { + public PipesProvider(RevitRepository revitRepository) : base(revitRepository) { + } + + public override MepClass MepClass => MepClass.Pipes; + + protected override ICollection GetSelectedElements() { + return _revitRepository.GetSelectedElements(BuiltInCategory.OST_PipeCurves).ToArray(); + } + + protected override ICollection GetElementsOnActiveView() { + return _revitRepository.GetActiveViewElements(BuiltInCategory.OST_PipeCurves).ToArray(); + } + + protected override ICollection GetElementsInDocument() { + return _revitRepository.GetElements(BuiltInCategory.OST_PipeCurves).ToArray(); + } + + protected override SplittableElement CreateSplittable(MEPCurve curve, ICollection all) { + var filtered = FilterDisplacements(curve, all); + return new SplittablePipe((Pipe) curve, filtered); + } +} diff --git a/src/RevitSplitMepCurve/ViewModels/Errors/ErrorViewModel.cs b/src/RevitSplitMepCurve/ViewModels/Errors/ErrorViewModel.cs new file mode 100644 index 000000000..0505c1fe6 --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/Errors/ErrorViewModel.cs @@ -0,0 +1,23 @@ +using System; + +using Autodesk.Revit.DB; + +using dosymep.WPF.ViewModels; + +using RevitSplitMepCurve.Models.Errors; + +namespace RevitSplitMepCurve.ViewModels.Errors; + +internal class ErrorViewModel : BaseViewModel { + private readonly ErrorModel _error; + + public ErrorViewModel(ErrorModel error) { + _error = error ?? throw new ArgumentNullException(nameof(error)); + } + + public string Message => _error.Message; + + public string ElementName => _error.Element.Name; + + public Element Element => _error.Element; +} diff --git a/src/RevitSplitMepCurve/ViewModels/Errors/ErrorsViewModel.cs b/src/RevitSplitMepCurve/ViewModels/Errors/ErrorsViewModel.cs new file mode 100644 index 000000000..514f70d0b --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/Errors/ErrorsViewModel.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows.Input; + +using Autodesk.Revit.DB; + +using dosymep.WPF.Commands; +using dosymep.WPF.ViewModels; + +using RevitSplitMepCurve.Models; +using RevitSplitMepCurve.Services.Core; + +namespace RevitSplitMepCurve.ViewModels.Errors; + +internal class ErrorsViewModel : BaseViewModel { + private readonly RevitRepository _revitRepository; + private readonly IErrorsService _errorsService; + + public ErrorsViewModel(RevitRepository revitRepository, IErrorsService errorsService) { + _revitRepository = revitRepository ?? throw new ArgumentNullException(nameof(revitRepository)); + _errorsService = errorsService ?? throw new ArgumentNullException(nameof(errorsService)); + + ShowElementsCommand = RelayCommand.Create(ShowElements, CanShowElements); + LoadViewCommand = RelayCommand.Create(LoadView); + } + + public ICommand ShowElementsCommand { get; } + + public ICommand LoadViewCommand { get; } + + public ObservableCollection Errors { get; } = []; + + private void ShowElements(ErrorViewModel error) { + _revitRepository.ShowElements(error.Element); + } + + private bool CanShowElements(ErrorViewModel error) => error is not null; + + private void LoadView() { + Errors.Clear(); + foreach(var error in _errorsService.GetErrors()) { + Errors.Add(new ErrorViewModel(error)); + } + } +} diff --git a/src/RevitSplitMepCurve/ViewModels/LevelViewModel.cs b/src/RevitSplitMepCurve/ViewModels/LevelViewModel.cs new file mode 100644 index 000000000..b3fe99147 --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/LevelViewModel.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +using Autodesk.Revit.DB; + +using dosymep.SimpleServices; +using dosymep.WPF.ViewModels; + +namespace RevitSplitMepCurve.ViewModels; + +internal class LevelViewModel : BaseViewModel, IEquatable { + private readonly ILocalizationService _localization; + private bool _isSelected; + + /// + /// Вью модель уровня + /// + /// Уровень + /// Сервис локализации + public LevelViewModel( + Level level, + ILocalizationService localization) { + Level = level ?? throw new ArgumentNullException(nameof(level)); + _localization = localization ?? throw new ArgumentNullException(nameof(localization)); + + double elevationFt = level.Elevation - BasePoint.GetProjectBasePoint(level.Document).Position.Z; + double elevationMm = UnitUtils.ConvertFromInternalUnits(elevationFt, UnitTypeId.Millimeters); + DisplayName = _localization.GetLocalizedString("MainWindow.Levels.DisplayFormat", level.Name, elevationMm); + } + + public string Name => Level.Name; + + public string DisplayName { get; } + + public Level Level { get; } + + public bool IsSelected { + get => _isSelected; + set => RaiseAndSetIfChanged(ref _isSelected, value); + } + + public bool Equals(LevelViewModel other) { + if(other is null) { + return false; + } + + if(ReferenceEquals(this, other)) { + return true; + } + + return Level.Id == other.Level.Id; + } + + public override bool Equals(object obj) { + return Equals(obj as LevelViewModel); + } + + public override int GetHashCode() { + return EqualityComparer.Default.GetHashCode(Level.Id); + } +} diff --git a/src/RevitSplitMepCurve/ViewModels/MainViewModel.cs b/src/RevitSplitMepCurve/ViewModels/MainViewModel.cs new file mode 100644 index 000000000..5f9795eeb --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/MainViewModel.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Windows; +using System.Windows.Input; + +using dosymep.Revit; +using dosymep.SimpleServices; +using dosymep.WPF.Commands; +using dosymep.WPF.ViewModels; + +using RevitSplitMepCurve.Models; +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Errors; +using RevitSplitMepCurve.Models.Exceptions; +using RevitSplitMepCurve.Models.Splittable; +using RevitSplitMepCurve.Services.Core; +using RevitSplitMepCurve.Services.Providers; +using RevitSplitMepCurve.ViewModels.Providers; + +using Autodesk.Revit.DB; + +namespace RevitSplitMepCurve.ViewModels; + +internal class MainViewModel : BaseViewModel { + public IProgressDialogFactory ProgressFactory { get; } + private readonly PluginConfig _pluginConfig; + private readonly RevitRepository _revitRepository; + private readonly IErrorsService _errorsService; + private readonly ILocalizationService _localization; + private readonly ErrorsWindowService _errorsWindowService; + private readonly IMessageBoxService _messageBoxService; + private readonly ICollection _providers; + + private SelectionModeViewModel _selectionMode; + private ElementsProviderViewModel _elementsProvider; + private bool _showPlacingErrors; + private bool _selectAllLevels; + private string _errorText; + + public MainViewModel( + PluginConfig pluginConfig, + RevitRepository revitRepository, + IErrorsService errorsService, + ILocalizationService localization, + ErrorsWindowService errorsWindowService, + IMessageBoxService messageBoxService, + IProgressDialogFactory progressFactory, + ICollection providers) { + ProgressFactory = progressFactory ?? throw new ArgumentNullException(nameof(progressFactory)); + _pluginConfig = pluginConfig ?? throw new ArgumentNullException(nameof(pluginConfig)); + _revitRepository = revitRepository ?? throw new ArgumentNullException(nameof(revitRepository)); + _errorsService = errorsService ?? throw new ArgumentNullException(nameof(errorsService)); + _localization = localization ?? throw new ArgumentNullException(nameof(localization)); + _errorsWindowService = errorsWindowService ?? throw new ArgumentNullException(nameof(errorsWindowService)); + _messageBoxService = messageBoxService ?? throw new ArgumentNullException(nameof(messageBoxService)); + _providers = providers ?? throw new ArgumentNullException(nameof(providers)); + + Levels = []; + AvailableSelectionModes = []; + AvailableElementsProviders = []; + + LoadViewCommand = RelayCommand.Create(LoadView); + AcceptViewCommand = RelayCommand.Create(AcceptView, CanAcceptView); + } + + public ICommand LoadViewCommand { get; } + + public ICommand AcceptViewCommand { get; } + + public IMessageBoxService MessageBoxService => _messageBoxService; + + public SelectionModeViewModel SelectionMode { + get => _selectionMode; + set => RaiseAndSetIfChanged(ref _selectionMode, value); + } + + public ObservableCollection AvailableSelectionModes { get; } + + public ElementsProviderViewModel ElementsProvider { + get => _elementsProvider; + set => RaiseAndSetIfChanged(ref _elementsProvider, value); + } + + public ObservableCollection AvailableElementsProviders { get; } + + public ObservableCollection Levels { get; } + + public bool ShowPlacingErrors { + get => _showPlacingErrors; + set => RaiseAndSetIfChanged(ref _showPlacingErrors, value); + } + + public bool SelectAllLevels { + get => _selectAllLevels; + set { + if(_selectAllLevels == value) { + return; + } + RaiseAndSetIfChanged(ref _selectAllLevels, value); + foreach(var level in Levels) { + level.IsSelected = value; + } + } + } + + public string ErrorText { + get => _errorText; + private set => RaiseAndSetIfChanged(ref _errorText, value); + } + + private void LoadView() { + LoadConfig(); + } + + private PipesProviderViewModel CreatePipesProvider(PipesProvider provider, RevitSettings config) { + var providerVm = new PipesProviderViewModel(_localization, provider, _revitRepository); + providerVm.RoundSymbol.SelectedItem = + providerVm.RoundSymbol.AvailableItems.FirstOrDefault(s => + config?.RoundConnector?.Equals(s.Symbol) ?? false); + return providerVm; + } + + private DuctsProviderViewModel CreateDuctsProvider(DuctsProvider provider, RevitSettings config) { + var providerVm = new DuctsProviderViewModel(_localization, provider, _revitRepository); + providerVm.RoundSymbol.SelectedItem = + providerVm.RoundSymbol.AvailableItems + .FirstOrDefault(s => config?.RoundConnector?.Equals(s.Symbol) ?? false); + providerVm.RectangleSymbol.SelectedItem = + providerVm.RectangleSymbol.AvailableItems.FirstOrDefault(s => + config?.RectangleConnector?.Equals(s.Symbol) ?? false); + return providerVm; + } + + private void CheckSyncNecessity(ICollection elements) { + if(_revitRepository.IsSyncRequired(elements)) { + _messageBoxService.Show( + _localization.GetLocalizedString("MainWindow.SyncRequired"), + _localization.GetLocalizedString("MainWindow.Title"), + MessageBoxButton.OK, + MessageBoxImage.Warning); + throw new OperationCanceledException(); + } + } + + private ICollection GetSplittableElements(ICollection selectedLevels) { + return ElementsProvider.Provider.GetElements(SelectionMode.Mode) + .Where(e => e.CanBeSplitted(selectedLevels)) + .ToArray(); + } + + private ICollection GetSelectedLevels() { + return Levels + .Where(l => l.IsSelected) + .Select(l => l.Level) + .ToArray(); + } + + private void AcceptView() { + SaveConfig(); + var selectedLevels = GetSelectedLevels(); + var settings = ElementsProvider.GetSplitSettings(selectedLevels); + var splittable = GetSplittableElements(selectedLevels); + + CheckSyncNecessity(splittable); + + if(splittable.Count == 0) { + return; + } + + using(var dialogService = ProgressFactory.CreateDialog()) { + dialogService.MaxValue = splittable.Count; + var progress = dialogService.CreateProgress(); + var ct = dialogService.CreateCancellationToken(); + int i = 0; + dialogService.Show(); + using(var t = _revitRepository.Document.StartTransaction( + _localization.GetLocalizedString("MainWindow.TransactionName"))) { + foreach(var item in splittable) { + ct.ThrowIfCancellationRequested(); + progress.Report(++i); + using(var subT = new SubTransaction(_revitRepository.Document)) { + subT.Start(); + try { + var result = item.Split(settings); + result.UpdateSegments(); + subT.Commit(); + } catch(CannotGetConnectorSymbolException) { + _errorsService.AddError(item.Element, "Error.InsufficientSpace"); + subT.RollBack(); + } catch(CannotCreateConnectorException) { + _errorsService.AddError(item.Element, "Error.CannotCreateConnector"); + subT.RollBack(); + } + } + } + + t.Commit(); + } + } + + ShowErrors(); + } + + private bool CanAcceptView() { + if(ElementsProvider is null) { + ErrorText = _localization.GetLocalizedString("MainWindow.Validation.NoProvider"); + return false; + } + if(SelectionMode is null) { + ErrorText = _localization.GetLocalizedString("MainWindow.Validation.NoSelectionMode"); + return false; + } + + string providerError = ElementsProvider.GetErrorText(); + if(!string.IsNullOrWhiteSpace(providerError)) { + ErrorText = providerError; + return false; + } + if(!Levels.Any(l => l.IsSelected)) { + ErrorText = _localization.GetLocalizedString("MainWindow.Validation.NoLevels"); + return false; + } + ErrorText = null; + return true; + } + + private void ShowErrors() { + if(ShowPlacingErrors && _errorsService.ContainsErrors()) { + _errorsWindowService.ShowErrorsWindow(); + } + } + + private void SaveConfig() { + var setting = _pluginConfig.GetSettings(_revitRepository.Document) + ?? _pluginConfig.AddSettings(_revitRepository.Document); + + setting.SelectedMepClass = ElementsProvider?.MepClass ?? RevitSettings.DefaultMepClass; + setting.SelectedMode = _selectionMode?.Mode ?? RevitSettings.DefaultSelectionMode; + setting.UncheckedLevelNames = Levels + .Where(l => !l.IsSelected) + .Select(l => l.Name) + .ToList(); + setting.ShowSplitErrors = ShowPlacingErrors; + + if(ElementsProvider is PipesProviderViewModel pipes) { + setting.RoundConnector = new ConnectorConfig() { + SymbolName = pipes.RoundSymbol.SelectedItem?.Symbol.Name, + FamilyName = pipes.RoundSymbol.SelectedItem?.Symbol.FamilyName + }; + setting.RectangleConnector = null; + } else if(ElementsProvider is DuctsProviderViewModel ducts) { + setting.RoundConnector = new ConnectorConfig() { + SymbolName = ducts.RoundSymbol.SelectedItem?.Symbol.Name, + FamilyName = ducts.RoundSymbol.SelectedItem?.Symbol.FamilyName + }; + setting.RectangleConnector = new ConnectorConfig() { + SymbolName = ducts.RectangleSymbol.SelectedItem?.Symbol.Name, + FamilyName = ducts.RectangleSymbol.SelectedItem?.Symbol.FamilyName + }; + } + + _pluginConfig.SaveProjectConfig(); + } + + private void InitializeElementsProviders(RevitSettings config) { + foreach(var p in _providers) { + ElementsProviderViewModel vm = p switch { + PipesProvider pp => CreatePipesProvider(pp, config), + DuctsProvider dp => CreateDuctsProvider(dp, config), + _ => throw new InvalidOperationException() + }; + AvailableElementsProviders.Add(vm); + } + + ElementsProvider = AvailableElementsProviders + .FirstOrDefault(p => config != null && p.MepClass == config.SelectedMepClass) + ?? AvailableElementsProviders.First(); + } + + private void InitializeLevels(RevitSettings config) { + var levels = _revitRepository.GetLevels().OrderByDescending(l => l.Elevation); + foreach(var level in levels) { + var vm = new LevelViewModel(level, _localization); + vm.IsSelected = config is null + || !config.UncheckedLevelNames.Contains(level.Name, StringComparer.OrdinalIgnoreCase); + vm.PropertyChanged += OnLevelPropertyChanged; + Levels.Add(vm); + } + + _selectAllLevels = Levels.All(l => l.IsSelected); + OnPropertyChanged(nameof(SelectAllLevels)); + } + + private void InitializeSelectionModes(RevitSettings config) { + bool hasSelectedElements = _revitRepository.HasSelectedElements(); + foreach(var mode in Enum.GetValues(typeof(SelectionMode)).OfType()) { + var modeVm = new SelectionModeViewModel(_localization, mode); + if(mode == RevitSplitMepCurve.Models.Enums.SelectionMode.SelectedElements + && !hasSelectedElements) { + continue; + } + + AvailableSelectionModes.Add(modeVm); + } + + SelectionMode = hasSelectedElements + ? AvailableSelectionModes.First(m => + m.Mode == RevitSplitMepCurve.Models.Enums.SelectionMode.SelectedElements) + : (AvailableSelectionModes.FirstOrDefault(m => config != null && m.Mode == config.SelectedMode) + ?? AvailableSelectionModes.First()); + } + + private void OnLevelPropertyChanged(object sender, PropertyChangedEventArgs e) { + if(e.PropertyName != nameof(LevelViewModel.IsSelected)) { + return; + } + bool allSelected = Levels.All(l => l.IsSelected); + if(_selectAllLevels != allSelected) { + _selectAllLevels = allSelected; + OnPropertyChanged(nameof(SelectAllLevels)); + } + } + + private void LoadConfig() { + var config = _pluginConfig.GetSettings(_revitRepository.Document); + + InitializeLevels(config); + InitializeSelectionModes(config); + InitializeElementsProviders(config); + + ShowPlacingErrors = config?.ShowSplitErrors ?? true; + } +} diff --git a/src/RevitSplitMepCurve/ViewModels/Providers/DuctsProviderViewModel.cs b/src/RevitSplitMepCurve/ViewModels/Providers/DuctsProviderViewModel.cs new file mode 100644 index 000000000..c9d5a7a75 --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/Providers/DuctsProviderViewModel.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using System; + +using Autodesk.Revit.DB; + +using dosymep.SimpleServices; + +using RevitSplitMepCurve.Models; +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Settings; +using RevitSplitMepCurve.Services.Providers; +using RevitSplitMepCurve.ViewModels.Symbols; + +namespace RevitSplitMepCurve.ViewModels.Providers; + +internal class DuctsProviderViewModel : ElementsProviderViewModel { + public DuctsProviderViewModel( + ILocalizationService localization, + DuctsProvider provider, + RevitRepository revitRepository) : base(localization, provider) { + var roundSymbols = revitRepository + .GetConnectorSymbols(BuiltInCategory.OST_DuctFitting, ConnectorProfileType.Round) + .Select(s => new FamilySymbolViewModel(s)) + .ToArray(); + var rectangleSymbols = revitRepository + .GetConnectorSymbols(BuiltInCategory.OST_DuctFitting, ConnectorProfileType.Rectangular) + .Select(s => new FamilySymbolViewModel(s)) + .ToArray(); + RoundSymbol = new SelectableFamilySymbolViewModel( + localization.GetLocalizedString("MainWindow.ConnectorRound"), + roundSymbols); + RectangleSymbol = new SelectableFamilySymbolViewModel( + localization.GetLocalizedString("MainWindow.ConnectorRectangle"), + rectangleSymbols); + Name = _localization.GetLocalizedString( + $"{nameof(RevitSplitMepCurve.Models.Enums.MepClass)}.{provider.MepClass}"); + } + + public override string Name { get; } + + public override MepClass MepClass => MepClass.Ducts; + + public SelectableFamilySymbolViewModel RoundSymbol { get; } + + public SelectableFamilySymbolViewModel RectangleSymbol { get; } + + public override string GetErrorText() { + if(RoundSymbol.SelectedItem is null || RectangleSymbol.SelectedItem is null) { + return _localization.GetLocalizedString("MainWindow.Validation.NoConnector"); + } + return null; + } + + public override ISplitSettings GetSplitSettings(ICollection levels) { + if(RoundSymbol.SelectedItem is null + || RectangleSymbol.SelectedItem is null) { + throw new InvalidOperationException(); + } + return new SplitSettings( + RoundSymbol.SelectedItem.Symbol, + RectangleSymbol.SelectedItem.Symbol, + levels); + } +} diff --git a/src/RevitSplitMepCurve/ViewModels/Providers/ElementsProviderViewModel.cs b/src/RevitSplitMepCurve/ViewModels/Providers/ElementsProviderViewModel.cs new file mode 100644 index 000000000..bd881cf0b --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/Providers/ElementsProviderViewModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +using Autodesk.Revit.DB; + +using dosymep.SimpleServices; +using dosymep.WPF.ViewModels; + +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Settings; +using RevitSplitMepCurve.Services.Providers; + +namespace RevitSplitMepCurve.ViewModels.Providers; + +internal abstract class ElementsProviderViewModel : BaseViewModel { + protected readonly ILocalizationService _localization; + + protected ElementsProviderViewModel(ILocalizationService localization, IElementsProvider provider) { + _localization = localization ?? throw new ArgumentNullException(nameof(localization)); + Provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + public abstract string Name { get; } + + public abstract MepClass MepClass { get; } + + public IElementsProvider Provider { get; } + + /// Текст ошибки валидации. Пусто если всё ок. + public abstract string GetErrorText(); + + /// Настройки разделения для выбранных уровней и текущих соединителей. + public abstract ISplitSettings GetSplitSettings(ICollection levels); +} diff --git a/src/RevitSplitMepCurve/ViewModels/Providers/PipesProviderViewModel.cs b/src/RevitSplitMepCurve/ViewModels/Providers/PipesProviderViewModel.cs new file mode 100644 index 000000000..deb20bf35 --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/Providers/PipesProviderViewModel.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Autodesk.Revit.DB; + +using dosymep.SimpleServices; + +using RevitSplitMepCurve.Models; +using RevitSplitMepCurve.Models.Enums; +using RevitSplitMepCurve.Models.Settings; +using RevitSplitMepCurve.Services.Providers; +using RevitSplitMepCurve.ViewModels.Symbols; + +namespace RevitSplitMepCurve.ViewModels.Providers; + +internal class PipesProviderViewModel : ElementsProviderViewModel { + public PipesProviderViewModel( + ILocalizationService localization, + PipesProvider provider, + RevitRepository revitRepository) : base(localization, provider) { + var symbols = revitRepository + .GetConnectorSymbols(BuiltInCategory.OST_PipeFitting) + .Select(s => new FamilySymbolViewModel(s)) + .ToArray(); + RoundSymbol = new SelectableFamilySymbolViewModel( + localization.GetLocalizedString("MainWindow.ConnectorRound"), symbols); + Name = _localization.GetLocalizedString( + $"{nameof(RevitSplitMepCurve.Models.Enums.MepClass)}.{provider.MepClass}"); + } + + public override string Name { get; } + + public override MepClass MepClass => MepClass.Pipes; + + public SelectableFamilySymbolViewModel RoundSymbol { get; } + + public override string GetErrorText() { + if(RoundSymbol.SelectedItem is null) { + return _localization.GetLocalizedString("MainWindow.Validation.NoConnector"); + } + return null; + } + + public override ISplitSettings GetSplitSettings(ICollection levels) { + if(RoundSymbol.SelectedItem is null) { + throw new InvalidOperationException(); + } + + return new SplitSettings(RoundSymbol.SelectedItem.Symbol, null, levels); + } +} diff --git a/src/RevitSplitMepCurve/ViewModels/SelectionModeViewModel.cs b/src/RevitSplitMepCurve/ViewModels/SelectionModeViewModel.cs new file mode 100644 index 000000000..09a824503 --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/SelectionModeViewModel.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +using dosymep.SimpleServices; +using dosymep.WPF.ViewModels; + +using RevitSplitMepCurve.Models.Enums; + +namespace RevitSplitMepCurve.ViewModels; + +internal class SelectionModeViewModel : BaseViewModel, IEquatable { + public SelectionModeViewModel(ILocalizationService localization, SelectionMode mode) { + if(localization == null) { + throw new ArgumentNullException(nameof(localization)); + } + + Mode = mode; + Name = localization.GetLocalizedString($"{nameof(SelectionMode)}.{mode}"); + } + + public string Name { get; } + + public SelectionMode Mode { get; } + + public bool Equals(SelectionModeViewModel other) { + return other is not null && Mode == other.Mode; + } + + public override bool Equals(object obj) { + return Equals(obj as SelectionModeViewModel); + } + + public override int GetHashCode() { + return EqualityComparer.Default.GetHashCode(Mode); + } +} diff --git a/src/RevitSplitMepCurve/ViewModels/Symbols/FamilySymbolViewModel.cs b/src/RevitSplitMepCurve/ViewModels/Symbols/FamilySymbolViewModel.cs new file mode 100644 index 000000000..1e7d27ddf --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/Symbols/FamilySymbolViewModel.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +using Autodesk.Revit.DB; + +using dosymep.WPF.ViewModels; + +namespace RevitSplitMepCurve.ViewModels.Symbols; + +internal class FamilySymbolViewModel : BaseViewModel, IEquatable { + public FamilySymbolViewModel(FamilySymbol symbol) { + Symbol = symbol ?? throw new ArgumentNullException(nameof(symbol)); + Name = $"{symbol.FamilyName} : {symbol.Name}"; + } + + public string Name { get; } + + public FamilySymbol Symbol { get; } + + public bool Equals(FamilySymbolViewModel other) { + if(other is null) { + return false; + } + + if(ReferenceEquals(this, other)) { + return true; + } + + return Symbol.Id == other.Symbol.Id; + } + + public override bool Equals(object obj) { + return Equals(obj as FamilySymbolViewModel); + } + + public override int GetHashCode() { + return EqualityComparer.Default.GetHashCode(Symbol.Id); + } +} diff --git a/src/RevitSplitMepCurve/ViewModels/Symbols/SelectableFamilySymbolViewModel.cs b/src/RevitSplitMepCurve/ViewModels/Symbols/SelectableFamilySymbolViewModel.cs new file mode 100644 index 000000000..fa84d0198 --- /dev/null +++ b/src/RevitSplitMepCurve/ViewModels/Symbols/SelectableFamilySymbolViewModel.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Windows.Data; + +using dosymep.WPF.ViewModels; + +namespace RevitSplitMepCurve.ViewModels.Symbols; + +internal class SelectableFamilySymbolViewModel : BaseViewModel { + private FamilySymbolViewModel _selectedItem; + private string _searchText; + + public SelectableFamilySymbolViewModel(string label, ICollection availableItems) { + Label = label ?? throw new ArgumentNullException(nameof(label)); + AvailableItems = [.. availableItems.OrderBy(i => i.Name)]; + FilteredItems = new CollectionViewSource() { Source = AvailableItems }; + FilteredItems.Filter += SymbolsFilterHandler; + PropertyChanged += OnPropertyChanged; + } + + public string Label { get; } + + public FamilySymbolViewModel SelectedItem { + get => _selectedItem; + set => RaiseAndSetIfChanged(ref _selectedItem, value); + } + + public string SearchText { + get => _searchText; + set => RaiseAndSetIfChanged(ref _searchText, value); + } + + public ObservableCollection AvailableItems { get; } + + public CollectionViewSource FilteredItems { get; } + + private void SymbolsFilterHandler(object sender, FilterEventArgs e) { + if(e.Item is FamilySymbolViewModel symbol) { + if(!string.IsNullOrWhiteSpace(SearchText)) { + e.Accepted = symbol.Name.IndexOf(SearchText, StringComparison.CurrentCultureIgnoreCase) >= 0; + return; + } + + e.Accepted = true; + } + } + + private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { + if(e.PropertyName == nameof(SearchText)) { + FilteredItems?.View.Refresh(); + } + } +} diff --git a/src/RevitSplitMepCurve/Views/ErrorsWindow.xaml b/src/RevitSplitMepCurve/Views/ErrorsWindow.xaml new file mode 100644 index 000000000..8d8719be1 --- /dev/null +++ b/src/RevitSplitMepCurve/Views/ErrorsWindow.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/RevitSplitMepCurve/Views/ErrorsWindow.xaml.cs b/src/RevitSplitMepCurve/Views/ErrorsWindow.xaml.cs new file mode 100644 index 000000000..2d0404b77 --- /dev/null +++ b/src/RevitSplitMepCurve/Views/ErrorsWindow.xaml.cs @@ -0,0 +1,25 @@ +using dosymep.SimpleServices; + +namespace RevitSplitMepCurve.Views; + +public partial class ErrorsWindow { + public ErrorsWindow( + 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(RevitSplitMepCurve); + + public override string ProjectConfigName => nameof(ErrorsWindow); +} diff --git a/src/RevitSplitMepCurve/Views/MainWindow.xaml b/src/RevitSplitMepCurve/Views/MainWindow.xaml new file mode 100644 index 000000000..45c233a7a --- /dev/null +++ b/src/RevitSplitMepCurve/Views/MainWindow.xaml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/RevitSplitMepCurve/Views/MainWindow.xaml.cs b/src/RevitSplitMepCurve/Views/MainWindow.xaml.cs new file mode 100644 index 000000000..69a0ab382 --- /dev/null +++ b/src/RevitSplitMepCurve/Views/MainWindow.xaml.cs @@ -0,0 +1,36 @@ +using System.Windows; + +using dosymep.SimpleServices; + +namespace RevitSplitMepCurve.Views; + +public partial class MainWindow { + public MainWindow( + 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(RevitSplitMepCurve); + + public override string ProjectConfigName => nameof(MainWindow); + + private void ButtonOk_Click(object sender, RoutedEventArgs e) { + DialogResult = true; + } + + private void ButtonCancel_Click(object sender, RoutedEventArgs e) { + DialogResult = false; + } +} diff --git a/src/RevitSplitMepCurve/Views/Templates/ProvidersTemplates.xaml b/src/RevitSplitMepCurve/Views/Templates/ProvidersTemplates.xaml new file mode 100644 index 000000000..9f2528ae3 --- /dev/null +++ b/src/RevitSplitMepCurve/Views/Templates/ProvidersTemplates.xaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RevitSplitMepCurve/assets/Localization/Language.en-US.xaml b/src/RevitSplitMepCurve/assets/Localization/Language.en-US.xaml new file mode 100644 index 000000000..2d28aa880 --- /dev/null +++ b/src/RevitSplitMepCurve/assets/Localization/Language.en-US.xaml @@ -0,0 +1,74 @@ + + + Selected elements + Visible in view + Entire document + + Pipes + Ducts + + Split by Construction Level + Category + Selection mode + Levels to split + {0} ({1:+0;-0;0} mm) + Level elevations relative to project base point + Select all + Round connector + Rectangular connector + Show errors + OK + Cancel + Split by levels + Some elements are busy or outdated. Synchronize and try again. + Select a category + Select a selection mode + Select a connector + Select at least one level + + +Levels at the same elevation: +{0} + + Insufficient space for connector + Element split error + Failed to get connector type + Failed to create one of the connectors + + Split Report + Element + diff --git a/src/RevitSplitMepCurve/assets/Localization/Language.ru-RU.xaml b/src/RevitSplitMepCurve/assets/Localization/Language.ru-RU.xaml new file mode 100644 index 000000000..e7938244f --- /dev/null +++ b/src/RevitSplitMepCurve/assets/Localization/Language.ru-RU.xaml @@ -0,0 +1,74 @@ + + + Выбранные элементы + Видимые на виде + Весь документ + + Трубопроводы + Воздуховоды + + Разделить по уровню СМР + Категория + Режим выбора + Уровни к делению + {0} ({1:+0;-0;0} мм) + Отметки уровней относительно базовой точки проекта + Выбрать все + Круглый соединитель + Прямоугольный соединитель + Показывать ошибки + ОК + Отмена + Разделение по уровням + Некоторые элементы заняты или устарели. Синхронизируйтесь и повторите. + Выберите категорию + Выберите режим отбора элементов + Выберите соединитель + Отметьте хотя бы один уровень + + +Уровни на одной отметке: +{0} + + Недостаточно места для соединителя + Ошибка деления элемента + Не удалось получить типоразмер соединителя + Не удалось создать один из соединителей + + Отчёт о разделении + Элемент + diff --git a/src/RevitSplitMepCurve/assets/Styles/CustomStyles.xaml b/src/RevitSplitMepCurve/assets/Styles/CustomStyles.xaml new file mode 100644 index 000000000..6744943e4 --- /dev/null +++ b/src/RevitSplitMepCurve/assets/Styles/CustomStyles.xaml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file