feat(drawing): формат и ориентация листа (drawing_set_sheet_format)

Задать формат (A0–A5/user) и ориентацию (книжная/альбомная) листа чертежа через
API7 ILayoutSheet.Format (ISheetFormat). Стандартный формат — размеры авто; user —
явные width/height (landscape игнорируется, ориентация из соотношения сторон).

- DrawingService.SetSheetFormatAsync + RequireLayoutSheet
- PaperFormat (enum + Parse/ToKompas/FromKompas), SheetFormatResult
- DrawingValidation.ValidateFormatDimensions (user: >0; стандартный: размеры запрещены)
- 19 unit + 7 интеграционных тестов (всего 304 зелёных), сборка Release чистая

Спайк подтвердил: дефолт A4 portrait; стандартный формат пересчитывает W/H авто
(A3 landscape→420×297); user применяет width/height. Ревью спека pi/glm-5.1 + pi/kimi-k2.6
учтено (FromKompas для дружелюбного имени, запрет размеров у стандартного, sheetNumber>0,
landscape игнорируется для user).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 23:19:37 +03:00
parent adc9da37fa
commit 56ed2cb0cf
9 changed files with 470 additions and 0 deletions
@@ -84,6 +84,63 @@ public sealed class DrawingService
?? throw new InvalidOperationException("У листа оформления нет основной надписи (Stamp).");
}
/// <summary>Лист оформления чертежа по номеру (1-based) с диагностикой (номер &gt; 0, лист существует).</summary>
private ILayoutSheet RequireLayoutSheet(int sheetNumber)
{
if (sheetNumber <= 0)
throw new ArgumentOutOfRangeException(nameof(sheetNumber), "Номер листа должен быть > 0.");
var doc2d = RequireActiveDrawing();
var sheets = doc2d.LayoutSheets
?? throw new InvalidOperationException("Не удалось получить листы оформления чертежа.");
return sheets.ItemByNumber[sheetNumber] as ILayoutSheet
?? throw new InvalidOperationException($"На чертеже нет листа оформления #{sheetNumber}.");
}
/// <summary>
/// Задать формат (<paramref name="format"/>: A0A5 или user) и ориентацию листа
/// <paramref name="sheetNumber"/> активного чертежа. Для стандартного формата размеры авто
/// (<paramref name="width"/>/<paramref name="height"/> запрещены); для user — заданные width/height (мм),
/// при этом <paramref name="landscape"/> игнорируется (ориентация из соотношения сторон). Возвращает
/// итоговый формат, размеры и ориентацию (read-back из COM).
/// </summary>
public Task<SheetFormatResult> SetSheetFormatAsync(PaperFormat format, bool landscape, double width,
double height, int sheetNumber = 1, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => SetSheetFormatCore(format, landscape, width, height, sheetNumber), ct);
private SheetFormatResult SetSheetFormatCore(PaperFormat format, bool landscape, double width,
double height, int sheetNumber)
{
DrawingValidation.ValidateFormatDimensions(format, width, height);
var sheet = RequireLayoutSheet(sheetNumber);
var fmt = sheet.Format
?? throw new InvalidOperationException("У листа недоступен формат (Format == null).");
fmt.Format = PaperFormats.ToKompas(format);
if (format == PaperFormat.User)
{
// Для пользовательского ориентация задаётся соотношением width/height — VerticalOrientation не трогаем.
fmt.FormatWidth = width;
fmt.FormatHeight = height;
}
else
{
fmt.VerticalOrientation = !landscape; // стандартный: размеры авто по формату+ориентации
}
// FormatMultiplicity оставляем на COM-дефолте (1).
if (!sheet.Update())
throw new InvalidOperationException("ILayoutSheet.Update вернул FALSE — формат листа не задан.");
var fmt2 = sheet.Format
?? throw new InvalidOperationException("Формат листа недоступен после Update (Format == null).");
return new SheetFormatResult
{
Format = PaperFormats.FromKompas(fmt2.Format).ToString(),
Width = fmt2.FormatWidth,
Height = fmt2.FormatHeight,
Landscape = !fmt2.VerticalOrientation,
};
}
/// <summary>
/// Поставить линейный размер на виде активного чертежа между точками (x1,y1)-(x2,y2) в ЛОКАЛЬНОЙ
/// СК вида (мм); размерная линия проходит через (x3,y3). <paramref name="viewNumber"/> — номер вида
@@ -55,6 +55,26 @@ public static class DrawingValidation
throw new ArgumentException("Текст не должен быть пустым.", paramName);
}
/// <summary>
/// Проверить размеры формата листа: для <see cref="PaperFormat.User"/> — ширина и высота оба
/// конечны и &gt; 0; для стандартного формата — оба должны быть 0 (размеры авто, задавать нельзя).
/// </summary>
public static void ValidateFormatDimensions(PaperFormat format, double width, double height)
{
if (format == PaperFormat.User)
{
if (!double.IsFinite(width) || width <= 0 || !double.IsFinite(height) || height <= 0)
throw new ArgumentOutOfRangeException(nameof(width),
"Для format=user ширина и высота должны быть конечными и > 0.");
}
else if (width != 0 || height != 0)
{
throw new ArgumentException(
"Ширина/высота задаются только для format=user; для стандартного формата размеры авто (передайте 0).",
nameof(width));
}
}
/// <summary>Минимальное расстояние между выносными точками линейного размера (мм).</summary>
private const double MinPointDistance = 1e-7;
@@ -0,0 +1,70 @@
using Kompas6Constants;
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Формат листа чертежа (обёртка над <see cref="ksDocumentFormatEnum"/>).</summary>
public enum PaperFormat
{
/// <summary>A0 (841×1189 мм).</summary>
A0,
/// <summary>A1 (594×841 мм).</summary>
A1,
/// <summary>A2 (420×594 мм).</summary>
A2,
/// <summary>A3 (297×420 мм).</summary>
A3,
/// <summary>A4 (210×297 мм).</summary>
A4,
/// <summary>A5 (148×210 мм).</summary>
A5,
/// <summary>Пользовательский формат с явной шириной/высотой.</summary>
User,
}
/// <summary>Маппинг формата листа ↔ перечисление КОМПАС и разбор строки.</summary>
public static class PaperFormats
{
public static ksDocumentFormatEnum ToKompas(PaperFormat format) => format switch
{
PaperFormat.A0 => ksDocumentFormatEnum.ksFormatA0,
PaperFormat.A1 => ksDocumentFormatEnum.ksFormatA1,
PaperFormat.A2 => ksDocumentFormatEnum.ksFormatA2,
PaperFormat.A3 => ksDocumentFormatEnum.ksFormatA3,
PaperFormat.A4 => ksDocumentFormatEnum.ksFormatA4,
PaperFormat.A5 => ksDocumentFormatEnum.ksFormatA5,
PaperFormat.User => ksDocumentFormatEnum.ksFormatUser,
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Неизвестный формат листа."),
};
/// <summary>Обратный маппинг (для read-back дружелюбного имени).</summary>
public static PaperFormat FromKompas(ksDocumentFormatEnum format) => format switch
{
ksDocumentFormatEnum.ksFormatA0 => PaperFormat.A0,
ksDocumentFormatEnum.ksFormatA1 => PaperFormat.A1,
ksDocumentFormatEnum.ksFormatA2 => PaperFormat.A2,
ksDocumentFormatEnum.ksFormatA3 => PaperFormat.A3,
ksDocumentFormatEnum.ksFormatA4 => PaperFormat.A4,
ksDocumentFormatEnum.ksFormatA5 => PaperFormat.A5,
ksDocumentFormatEnum.ksFormatUser => PaperFormat.User,
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Неизвестный формат листа КОМПАС."),
};
/// <summary>Разобрать строку формата (A0..A5 / user, регистр/пробелы) в <see cref="PaperFormat"/>.</summary>
public static PaperFormat Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
return value.Trim().ToLowerInvariant() switch
{
"a0" => PaperFormat.A0,
"a1" => PaperFormat.A1,
"a2" => PaperFormat.A2,
"a3" => PaperFormat.A3,
"a4" => PaperFormat.A4,
"a5" => PaperFormat.A5,
"user" or "custom" or "пользовательский" => PaperFormat.User,
_ => throw new ArgumentException(
$"Неизвестный формат листа '{value}'. Допустимо: A0, A1, A2, A3, A4, A5, user.",
nameof(value)),
};
}
}
@@ -0,0 +1,17 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Результат задания формата листа чертежа (read-back из COM).</summary>
public sealed record SheetFormatResult
{
/// <summary>Итоговый формат («A0»…«A5» или «User»).</summary>
public required string Format { get; init; }
/// <summary>Ширина листа, мм.</summary>
public required double Width { get; init; }
/// <summary>Высота листа, мм.</summary>
public required double Height { get; init; }
/// <summary>Альбомная ориентация (true) либо книжная (false).</summary>
public required bool Landscape { get; init; }
}
+20
View File
@@ -187,4 +187,24 @@ public sealed class DrawingTools(KompasSession session, DrawingService drawing)
var lines = await drawing.SetTechnicalRequirementsAsync(text);
return $"Технические требования заданы ({lines} стр.).";
}
[McpServerTool(Name = "drawing_set_sheet_format")]
[Description("Задать формат и ориентацию листа активного чертежа. format: A0|A1|A2|A3|A4|A5|user. " +
"landscape: false = книжная (portrait), true = альбомная (landscape). Для стандартного формата " +
"размеры вычисляются автоматически — width/height передавать НЕ нужно (должны быть 0). Для format=user " +
"задайте width и height в мм (>0); при user флаг landscape игнорируется (ориентация — из соотношения " +
"сторон). sheetNumber — номер листа (1 = первый). Возвращает итоговый формат, размеры и ориентацию.")]
public async Task<string> SetSheetFormat(
[Description("Формат: A0|A1|A2|A3|A4|A5|user")] string format = "A4",
[Description("Альбомная ориентация (true) или книжная (false)")] bool landscape = false,
[Description("Ширина листа, мм (только для format=user)")] double width = 0,
[Description("Высота листа, мм (только для format=user)")] double height = 0,
[Description("Номер листа (1 = первый)")] int sheetNumber = 1)
{
await session.ConnectAsync();
var f = PaperFormats.Parse(format);
var r = await drawing.SetSheetFormatAsync(f, landscape, width, height, sheetNumber);
var orient = r.Landscape ? "альбомная" : "книжная";
return $"Формат листа {sheetNumber}: {r.Format}, {r.Width:0.#}×{r.Height:0.#} мм, {orient}.";
}
}