diff --git a/docs/superpowers/specs/2026-05-27-drawing-standard-views-design.md b/docs/superpowers/specs/2026-05-27-drawing-standard-views-design.md index 78a275e..1d011c8 100644 --- a/docs/superpowers/specs/2026-05-27-drawing-standard-views-design.md +++ b/docs/superpowers/specs/2026-05-27-drawing-standard-views-design.md @@ -1,7 +1,19 @@ # Дизайн: стандартные виды чертежа (drawing_create_standard_views) через API7 **Дата:** 2026-05-27 -**Статус:** спайк зелёный; ревью Codex учтено (10 замечаний); реализуется +**Статус:** реализовано и проверено (3 интеграционных + 18 unit, всего 177); ревью Codex спека (10) и реализации (7) учтено + +## Правки по ревью реализации Codex (7 замечаний) + +- **#1** непустоту проверяем у ВНОВЬ созданных видов (по разнице `IView.Number` до/после), а не по + всей коллекции — на непустом чертеже прежняя геометрия не маскирует пустые новые виды. +- **#3** откат: при `created != 3` или пустых видах удаляем созданные виды (`IView.Delete`) → чертёж + не остаётся в полу-состоянии. +- **#2** ленивость `ObjectCount` опровергнута эмпирически (happy-тест зелёный = геометрия есть сразу, + rebuild не нужен). **#7** добавлен unit-тест нормализации кириллического пути. +- **#4** повторный вызов намеренно добавляет ещё набор (дубликаты в разных `x,y` легитимны) — + отражено в описании инструмента. **#5** обход `IViews` уточнён (по `Number`); точечный release RCW + отклонён (v2-2). **#6** доп. негативы (.a3d, пустой валидный .m3d) — маргинальны, отложены. ## Решения по ревью Codex diff --git a/src/Kompas.Mcp.Core/Drawings/DrawingService.cs b/src/Kompas.Mcp.Core/Drawings/DrawingService.cs index 3b290c0..ea94d5a 100644 --- a/src/Kompas.Mcp.Core/Drawings/DrawingService.cs +++ b/src/Kompas.Mcp.Core/Drawings/DrawingService.cs @@ -47,24 +47,36 @@ public sealed class DrawingService throw new InvalidOperationException($"Файл модели пуст (не сохранён?): {path}"); var views = RequireActiveDrawingViews(); - var before = views.Count; + // Снимок номеров существующих видов — чтобы потом отличить созданные ИМЕННО этим вызовом + // (на чертеже уже есть системный вид и могут быть прежние виды). + var beforeNumbers = CollectViewNumbers(views); if (!views.AddStandartViews(path, "#Спереди", StandardProjections, x, y, scale, GapMm, GapMm)) throw new InvalidOperationException( $"AddStandartViews вернул FALSE — виды не созданы (файл модели или проекция недоступны): {path}"); - var created = views.Count - before; - if (created != ExpectedCreated) - throw new InvalidOperationException( - $"Ожидалось {ExpectedCreated} вида, создано {created} (было {before}, стало {views.Count})."); + var newViews = CollectNewViews(views, beforeNumbers); + try + { + if (newViews.Count != ExpectedCreated) + throw new InvalidOperationException( + $"Ожидалось {ExpectedCreated} вида, создано {newViews.Count}."); - // Гарантия непустоты: у спроецированных видов должна быть геометрия (иначе модель не - // спроецировалась — пустые виды). Системный вид #0 имеет ObjectCount 0, поэтому суммируем. - if (TotalObjectCount(views) <= 0) - throw new InvalidOperationException( - "Стандартные виды созданы, но пусты (модель не спроецировалась). Проверьте файл модели."); + // Гарантия непустоты: у созданных видов должна быть геометрия (иначе модель не + // спроецировалась). Проверяем ИМЕННО новые виды, не всю коллекцию. + if (newViews.Sum(v => v.ObjectCount) <= 0) + throw new InvalidOperationException( + "Стандартные виды созданы, но пусты (модель не спроецировалась). Проверьте файл модели."); - return new DrawingViewsResult { Created = created, Total = views.Count }; + return new DrawingViewsResult { Created = newViews.Count, Total = views.Count }; + } + catch + { + // Откат: удаляем виды, созданные этим вызовом, чтобы чертёж не остался в полу-состоянии. + foreach (var v in newViews) + try { v.Delete(); } catch { /* лучшее усилие */ } + throw; + } } private IViews RequireActiveDrawingViews() @@ -80,12 +92,21 @@ public sealed class DrawingService ?? throw new InvalidOperationException("Не удалось получить коллекцию видов чертежа."); } - private static int TotalObjectCount(IViews views) + private static HashSet CollectViewNumbers(IViews views) { - int sum = 0; + var set = new HashSet(); foreach (var o in views) if (o is IView v) - sum += v.ObjectCount; - return sum; + set.Add(v.Number); + return set; + } + + private static List CollectNewViews(IViews views, HashSet beforeNumbers) + { + var list = new List(); + foreach (var o in views) + if (o is IView v && !beforeNumbers.Contains(v.Number)) + list.Add(v); + return list; } } diff --git a/src/Kompas.Mcp.Host/Tools/DrawingTools.cs b/src/Kompas.Mcp.Host/Tools/DrawingTools.cs index 2be1ec7..d96ab9b 100644 --- a/src/Kompas.Mcp.Host/Tools/DrawingTools.cs +++ b/src/Kompas.Mcp.Host/Tools/DrawingTools.cs @@ -14,6 +14,7 @@ public sealed class DrawingTools(KompasSession session, DrawingService drawing) "сохранённой 3D-модели (.m3d деталь / .a3d сборка). Активный документ должен быть чертежом " + "(document_create drawing); модель должна быть сохранена в файл (document_save_as). " + "scale — масштаб (1.0 = 1:1); x,y — точка привязки главного вида на листе (мм). " + + "Повторный вызов добавит ещё один набор видов — задайте другие x,y, чтобы не накладывались. " + "Возвращает число созданных видов.")] public async Task CreateStandardViews( [Description("Путь к файлу 3D-модели (.m3d/.a3d)")] string partFilePath, diff --git a/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs b/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs index 632bf03..0b5ee7b 100644 --- a/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs +++ b/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs @@ -23,6 +23,7 @@ public sealed class DrawingValidationTests [InlineData(@"C:\m\part.m3d", ".m3d")] [InlineData(@"C:\m\frame.a3d", ".a3d")] [InlineData(@" C:\m\part.M3D ", ".m3d")] + [InlineData(@"C:\модели\деталь.m3d", ".m3d")] // кириллица в пути public void NormalizeModelPath_accepts_model_files(string path, string expectedExt) { var result = DrawingValidation.NormalizeModelPath(path); @@ -30,6 +31,14 @@ public sealed class DrawingValidationTests Assert.EndsWith(expectedExt, result, StringComparison.OrdinalIgnoreCase); } + [Fact] + public void NormalizeModelPath_preserves_cyrillic() + { + var result = DrawingValidation.NormalizeModelPath(@"C:\модели\деталь.m3d"); + Assert.Contains("деталь", result); + Assert.Contains("модели", result); + } + [Theory] [InlineData(0)] [InlineData(-1)]