feat: drawing_create_standard_views — упрочнение по ревью Codex реализации

#1: непустота проверяется у ВНОВЬ созданных видов (разница IView.Number), не по
всей коллекции — прежняя геометрия чертежа не маскирует пустые новые виды.
#3: откат созданных видов (IView.Delete) при created!=3 или пустых видах.
#7: unit-тест нормализации кириллического пути. #4: повторный вызов (дубликаты)
отражён в описании инструмента. #5/#6/#2 — по обоснованию (release RCW отклонён v2-2).

Тесты: 116 unit + 61 integration = 177 зелёных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 14:37:35 +03:00
parent de063290f4
commit 5aa4221a37
4 changed files with 59 additions and 16 deletions
@@ -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
+36 -15
View File
@@ -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<int> CollectViewNumbers(IViews views)
{
int sum = 0;
var set = new HashSet<int>();
foreach (var o in views)
if (o is IView v)
sum += v.ObjectCount;
return sum;
set.Add(v.Number);
return set;
}
private static List<IView> CollectNewViews(IViews views, HashSet<int> beforeNumbers)
{
var list = new List<IView>();
foreach (var o in views)
if (o is IView v && !beforeNumbers.Contains(v.Number))
list.Add(v);
return list;
}
}
@@ -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<string> CreateStandardViews(
[Description("Путь к файлу 3D-модели (.m3d/.a3d)")] string partFilePath,
@@ -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)]