diff --git a/src/Kompas.Mcp.Core/Documents/DocumentInfo.cs b/src/Kompas.Mcp.Core/Documents/DocumentInfo.cs new file mode 100644 index 0000000..b2bb660 --- /dev/null +++ b/src/Kompas.Mcp.Core/Documents/DocumentInfo.cs @@ -0,0 +1,14 @@ +namespace Kompas.Mcp.Core.Documents; + +/// Сводка по документу КОМПАС. +public sealed record DocumentInfo +{ + /// Имя файла без пути (или имя по умолчанию для несохранённого). + public required string Name { get; init; } + + /// Полный путь файла; пусто, если документ ещё не сохранён. + public required string PathName { get; init; } + + /// Тип документа. + public required KompasDocumentType Type { get; init; } +} diff --git a/src/Kompas.Mcp.Core/Documents/DocumentService.cs b/src/Kompas.Mcp.Core/Documents/DocumentService.cs new file mode 100644 index 0000000..b3311bf --- /dev/null +++ b/src/Kompas.Mcp.Core/Documents/DocumentService.cs @@ -0,0 +1,70 @@ +using System.Runtime.Versioning; +using Kompas.Mcp.Core.Threading; +using Kompas6Constants; +using KompasAPI7; + +namespace Kompas.Mcp.Core.Documents; + +/// Жизненный цикл документов КОМПАС через API7 (). +[SupportedOSPlatform("windows")] +public sealed class DocumentService +{ + private readonly KompasSession _session; + private readonly KompasDispatcher _dispatcher; + + public DocumentService(KompasSession session, KompasDispatcher dispatcher) + { + _session = session ?? throw new ArgumentNullException(nameof(session)); + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + } + + /// Создать новый документ заданного типа и сделать его активным. + public Task CreateAsync(KompasDocumentType type, bool visible = true, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + var doc = (IKompasDocument)_session.Application.Documents.Add(DocumentTypes.ToEnum(type), visible); + return ToInfo(doc); + }, ct); + + /// Открыть документ с диска. + public Task OpenAsync(string path, bool readOnly = false, bool visible = true, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + var doc = (IKompasDocument)_session.Application.Documents.Open(path, visible, readOnly); + if (doc is null) + throw new InvalidOperationException($"Не удалось открыть документ: {path}"); + return ToInfo(doc); + }, ct); + + /// Сводка по активному документу или null, если документов нет. + public Task GetActiveAsync(CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + var doc = _session.Application.ActiveDocument; + return doc is null ? null : ToInfo(doc); + }, ct); + + /// Сохранить активный документ (для уже сохранённого — без диалога). + public Task SaveAsync(CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => RequireActive().Save(), ct); + + /// Сохранить активный документ по указанному пути (безопасно для новых документов). + public Task SaveAsAsync(string path, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => RequireActive().SaveAs(path), ct); + + /// Закрыть активный документ. — сохранять ли изменения. + public Task CloseAsync(bool save = false, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + RequireActive().Close(save ? DocumentCloseOptions.kdSaveChanges : DocumentCloseOptions.kdDoNotSaveChanges), ct); + + private IKompasDocument RequireActive() + => _session.Application.ActiveDocument + ?? throw new InvalidOperationException("Нет активного документа."); + + private static DocumentInfo ToInfo(IKompasDocument doc) => new() + { + Name = doc.Name ?? string.Empty, + PathName = doc.PathName ?? string.Empty, + Type = DocumentTypes.FromEnum(doc.DocumentType), + }; +} diff --git a/src/Kompas.Mcp.Core/Documents/KompasDocumentType.cs b/src/Kompas.Mcp.Core/Documents/KompasDocumentType.cs new file mode 100644 index 0000000..1da7bdb --- /dev/null +++ b/src/Kompas.Mcp.Core/Documents/KompasDocumentType.cs @@ -0,0 +1,58 @@ +using Kompas6Constants; + +namespace Kompas.Mcp.Core.Documents; + +/// Дружелюбный тип документа КОМПАС (обёртка над ). +public enum KompasDocumentType +{ + Part, + Assembly, + Drawing, + Fragment, + Specification, + Text, +} + +/// Маппинг дружелюбного типа документа ↔ перечисление API7 ↔ строка. +public static class DocumentTypes +{ + public static DocumentTypeEnum ToEnum(KompasDocumentType type) => type switch + { + KompasDocumentType.Part => DocumentTypeEnum.ksDocumentPart, + KompasDocumentType.Assembly => DocumentTypeEnum.ksDocumentAssembly, + KompasDocumentType.Drawing => DocumentTypeEnum.ksDocumentDrawing, + KompasDocumentType.Fragment => DocumentTypeEnum.ksDocumentFragment, + KompasDocumentType.Specification => DocumentTypeEnum.ksDocumentSpecification, + KompasDocumentType.Text => DocumentTypeEnum.ksDocumentTextual, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Неизвестный тип документа."), + }; + + public static KompasDocumentType FromEnum(DocumentTypeEnum type) => type switch + { + DocumentTypeEnum.ksDocumentPart => KompasDocumentType.Part, + DocumentTypeEnum.ksDocumentAssembly => KompasDocumentType.Assembly, + DocumentTypeEnum.ksDocumentDrawing => KompasDocumentType.Drawing, + DocumentTypeEnum.ksDocumentFragment => KompasDocumentType.Fragment, + DocumentTypeEnum.ksDocumentSpecification => KompasDocumentType.Specification, + DocumentTypeEnum.ksDocumentTextual => KompasDocumentType.Text, + _ => KompasDocumentType.Part, + }; + + /// Разобрать строку ("part", "assembly", "drawing", "fragment", ...) в тип. + public static KompasDocumentType Parse(string value) + { + ArgumentNullException.ThrowIfNull(value); + return value.Trim().ToLowerInvariant() switch + { + "part" or "деталь" => KompasDocumentType.Part, + "assembly" or "сборка" => KompasDocumentType.Assembly, + "drawing" or "чертеж" or "чертёж" => KompasDocumentType.Drawing, + "fragment" or "фрагмент" => KompasDocumentType.Fragment, + "specification" or "спецификация" => KompasDocumentType.Specification, + "text" or "txt" or "текст" => KompasDocumentType.Text, + _ => throw new ArgumentException( + $"Неизвестный тип документа '{value}'. Допустимо: part, assembly, drawing, fragment, specification, text.", + nameof(value)), + }; + } +} diff --git a/tests/Kompas.Mcp.Tests/Integration/DocumentTests.cs b/tests/Kompas.Mcp.Tests/Integration/DocumentTests.cs new file mode 100644 index 0000000..dc803ab --- /dev/null +++ b/tests/Kompas.Mcp.Tests/Integration/DocumentTests.cs @@ -0,0 +1,42 @@ +using Kompas.Mcp.Core.Documents; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: жизненный цикл документов. +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class DocumentTests +{ + private readonly DocumentService _docs; + + public DocumentTests(KompasFixture fx) + => _docs = new DocumentService(fx.Session, fx.Dispatcher); + + [Fact] + public async Task Create_part_saveas_close_roundtrip() + { + var created = await _docs.CreateAsync(KompasDocumentType.Part); + Assert.Equal(KompasDocumentType.Part, created.Type); + + var path = TestPaths.NewFile(".m3d"); + await _docs.SaveAsAsync(path); + + var active = await _docs.GetActiveAsync(); + Assert.NotNull(active); + Assert.Equal(path, active!.PathName, ignoreCase: true); + + var closed = await _docs.CloseAsync(save: false); + Assert.True(closed); + Assert.True(File.Exists(path)); + } + + [Fact] + public async Task Create_fragment_reports_type() + { + await _docs.CreateAsync(KompasDocumentType.Fragment); + var active = await _docs.GetActiveAsync(); + Assert.NotNull(active); + Assert.Equal(KompasDocumentType.Fragment, active!.Type); + await _docs.CloseAsync(save: false); + } +} diff --git a/tests/Kompas.Mcp.Tests/TestPaths.cs b/tests/Kompas.Mcp.Tests/TestPaths.cs new file mode 100644 index 0000000..bcdab34 --- /dev/null +++ b/tests/Kompas.Mcp.Tests/TestPaths.cs @@ -0,0 +1,19 @@ +namespace Kompas.Mcp.Tests; + +/// Пути для тестовых артефактов: корень репозитория и gitignore-папка .scratch. +internal static class TestPaths +{ + public static string RepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "KompasMcp.slnx"))) + dir = dir.Parent; + return dir?.FullName ?? AppContext.BaseDirectory; + } + + public static string Scratch + => Directory.CreateDirectory(Path.Combine(RepoRoot(), ".scratch")).FullName; + + public static string NewFile(string extension) + => Path.Combine(Scratch, $"test_{DateTime.Now:HHmmss}_{Guid.NewGuid():N}{extension}"); +}